iris-pex-embedded-python 3.7.2b1__py3-none-any.whl → 3.7.2b2__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.
grongier/pex/__init__.py CHANGED
@@ -6,11 +6,16 @@ from iop._inbound_adapter import _InboundAdapter
6
6
  from iop._message import _Message
7
7
  from iop._message import _PickleMessage
8
8
  from iop._outbound_adapter import _OutboundAdapter
9
+ from iop._polling_business_service import _PollingBusinessServiceMixin
9
10
  from iop._persistent_message import Field as Field
10
11
  from iop._persistent_message import Model as Model
11
12
  from iop._persistent_message import _PersistentMessage
12
13
  from iop._private_session_duplex import _PrivateSessionDuplex
13
14
  from iop._private_session_process import _PrivateSessionProcess
15
+ from iop._settings import Category as Category
16
+ from iop._settings import Setting as Setting
17
+ from iop._settings import controls as controls
18
+ from iop._settings import setting as setting
14
19
  from iop._utils import _Utils
15
20
 
16
21
 
@@ -30,6 +35,10 @@ class BusinessService(_BusinessService):
30
35
  pass
31
36
 
32
37
 
38
+ class PollingBusinessService(_PollingBusinessServiceMixin, BusinessService):
39
+ pass
40
+
41
+
33
42
  class BusinessOperation(_BusinessOperation):
34
43
  pass
35
44
 
iop/__init__.py CHANGED
@@ -10,11 +10,16 @@ from iop._message import (
10
10
  _PydanticPickleMessage,
11
11
  )
12
12
  from iop._outbound_adapter import _OutboundAdapter
13
+ from iop._polling_business_service import _PollingBusinessServiceMixin
13
14
  from iop._persistent_message import Field as Field
14
15
  from iop._persistent_message import Model as Model
15
16
  from iop._persistent_message import _PersistentMessage
16
17
  from iop._private_session_duplex import _PrivateSessionDuplex
17
18
  from iop._private_session_process import _PrivateSessionProcess
19
+ from iop._settings import Category as Category
20
+ from iop._settings import Setting as Setting
21
+ from iop._settings import controls as controls
22
+ from iop._settings import setting as setting
18
23
  from iop._utils import _Utils
19
24
 
20
25
 
@@ -34,6 +39,10 @@ class BusinessService(_BusinessService):
34
39
  pass
35
40
 
36
41
 
42
+ class PollingBusinessService(_PollingBusinessServiceMixin, BusinessService):
43
+ pass
44
+
45
+
37
46
  class BusinessOperation(_BusinessOperation):
38
47
  pass
39
48
 
iop/_cli.py CHANGED
@@ -13,6 +13,7 @@ from importlib.metadata import version
13
13
  from ._local import _LocalDirector
14
14
  from ._remote import _RemoteDirector, get_remote_settings
15
15
  from ._director_protocol import DirectorProtocol
16
+ from ._utils import _Utils
16
17
 
17
18
 
18
19
  class CommandType(Enum):
@@ -57,6 +58,7 @@ class CommandArgs:
57
58
  force_local: bool = False
58
59
  remote_settings: Optional[str] = None
59
60
  update: bool = False
61
+ migration_plan: bool = False
60
62
 
61
63
 
62
64
  class Command:
@@ -252,7 +254,27 @@ class Command:
252
254
  if migrate_path is not None:
253
255
  if not os.path.isabs(migrate_path):
254
256
  migrate_path = os.path.join(os.getcwd(), migrate_path)
257
+ mode = "REMOTE" if self._is_remote else "LOCAL"
258
+ if self.args.migration_plan:
259
+ print(
260
+ _Utils.explain_migration(
261
+ migrate_path, mode=mode, namespace=self.director.namespace
262
+ )
263
+ )
264
+ return
265
+ if self._is_remote:
266
+ print(
267
+ _Utils.explain_migration(
268
+ migrate_path, mode=mode, namespace=self.director.namespace
269
+ )
270
+ )
255
271
  self.director.migrate(migrate_path)
272
+ if self._is_remote:
273
+ print(
274
+ _Utils.format_migration_success(
275
+ migrate_path, namespace=self.director.namespace
276
+ )
277
+ )
256
278
 
257
279
  def _handle_log(self) -> None:
258
280
  if self.args.log == "not_set":
@@ -406,6 +428,13 @@ def create_parser() -> argparse.ArgumentParser:
406
428
  help="force local mode, skip remote even if REMOTE_SETTINGS or IOP_URL is present",
407
429
  action="store_true",
408
430
  )
431
+ migrate.add_argument(
432
+ "--dry-run",
433
+ "--explain",
434
+ dest="migration_plan",
435
+ help="show the migration plan and validation messages without writing to IRIS",
436
+ action="store_true",
437
+ )
409
438
 
410
439
  remote = main_parser.add_argument_group("remote arguments")
411
440
  remote.add_argument(
iop/_common.py CHANGED
@@ -1,11 +1,143 @@
1
1
  import abc
2
2
  import inspect
3
3
  import traceback
4
- from typing import Any, ClassVar, List, Optional, Tuple
4
+ from enum import Enum
5
+ from types import UnionType
6
+ from typing import (
7
+ Annotated,
8
+ Any,
9
+ ClassVar,
10
+ List,
11
+ Optional,
12
+ Tuple,
13
+ Union,
14
+ get_args,
15
+ get_origin,
16
+ get_type_hints,
17
+ )
5
18
 
6
19
  from . import _iris
7
- from ._log_manager import LogManager, logging
8
20
  from ._debugpy import debugpython
21
+ from ._log_manager import LogManager, logging
22
+ from ._settings import Setting
23
+
24
+
25
+ _NO_VALUE = object()
26
+
27
+ _EXCLUDED_SETTING_NAMES = {
28
+ "INFO_URL",
29
+ "ICON_URL",
30
+ "PERSISTENT_PROPERTY_LIST",
31
+ "log_to_console",
32
+ "logger",
33
+ "iris_handle",
34
+ "DISPATCH",
35
+ "adapter",
36
+ "Adapter",
37
+ "buffer",
38
+ "BusinessHost",
39
+ "business_host",
40
+ "business_host_python",
41
+ }
42
+
43
+ _PYTHON_TYPE_TO_IRIS = {
44
+ int: "Integer",
45
+ float: "Numeric",
46
+ complex: "Numeric",
47
+ bool: "Boolean",
48
+ str: "String",
49
+ }
50
+
51
+ _SIMPLE_TYPE_NAMES = {
52
+ "int": "Integer",
53
+ "integer": "Integer",
54
+ "float": "Numeric",
55
+ "complex": "Numeric",
56
+ "number": "Numeric",
57
+ "numeric": "Numeric",
58
+ "bool": "Boolean",
59
+ "boolean": "Boolean",
60
+ "str": "String",
61
+ "string": "String",
62
+ }
63
+
64
+
65
+ def _string_metadata(value: Any) -> str:
66
+ if value is None:
67
+ return ""
68
+ if isinstance(value, Enum):
69
+ return str(value.value)
70
+ return str(value)
71
+
72
+
73
+ def _type_hints_with_extras(cls) -> dict[str, Any]:
74
+ try:
75
+ return get_type_hints(cls, include_extras=True)
76
+ except Exception:
77
+ hints: dict[str, Any] = {}
78
+ for base in reversed(inspect.getmro(cls)):
79
+ hints.update(getattr(base, "__annotations__", {}))
80
+ return hints
81
+
82
+
83
+ def _unwrap_annotated(data_type: Any) -> tuple[Any, tuple[Any, ...]]:
84
+ if get_origin(data_type) is Annotated:
85
+ args = get_args(data_type)
86
+ if args:
87
+ return args[0], args[1:]
88
+ return data_type, ()
89
+
90
+
91
+ def _setting_from_annotation(data_type: Any) -> tuple[Any, Optional[Setting]]:
92
+ data_type, metadata = _unwrap_annotated(data_type)
93
+ setting = None
94
+ for item in metadata:
95
+ if isinstance(item, Setting):
96
+ setting = item
97
+ return data_type, setting
98
+
99
+
100
+ def _unwrap_optional(data_type: Any) -> Any:
101
+ origin = get_origin(data_type)
102
+ if origin in (Union, UnionType):
103
+ args = [arg for arg in get_args(data_type) if arg is not type(None)]
104
+ if len(args) == 1:
105
+ return args[0]
106
+ return data_type
107
+
108
+
109
+ def _iris_data_type(data_type: Any) -> Optional[str]:
110
+ if data_type is None or data_type == "":
111
+ return None
112
+
113
+ data_type, _ = _unwrap_annotated(data_type)
114
+ data_type = _unwrap_optional(data_type)
115
+
116
+ if data_type is Any or data_type is object:
117
+ return "String"
118
+
119
+ if isinstance(data_type, str):
120
+ data_type = data_type.strip()
121
+ return _SIMPLE_TYPE_NAMES.get(data_type.lower(), data_type)
122
+
123
+ if data_type in _PYTHON_TYPE_TO_IRIS:
124
+ return _PYTHON_TYPE_TO_IRIS[data_type]
125
+
126
+ origin = get_origin(data_type)
127
+ if origin in _PYTHON_TYPE_TO_IRIS:
128
+ return _PYTHON_TYPE_TO_IRIS[origin]
129
+ if origin is not None:
130
+ return "String"
131
+
132
+ return None
133
+
134
+
135
+ def _is_setting_member(name: str, value: Any) -> bool:
136
+ if name.startswith("_") or name in _EXCLUDED_SETTING_NAMES:
137
+ return False
138
+ return not (
139
+ inspect.ismethod(value) or inspect.isfunction(value) or inspect.isclass(value)
140
+ )
9
141
 
10
142
 
11
143
  class _Common(metaclass=abc.ABCMeta):
@@ -168,87 +300,117 @@ class _Common(metaclass=abc.ABCMeta):
168
300
  - Required flag
169
301
  - Category
170
302
  - Description
303
+ - Control/editor context
171
304
 
172
305
  Only includes non-private class attributes and properties.
173
306
  """
174
307
  ret = []
175
308
  try:
176
- # getmembers() returns all the members of an obj
177
- for member in inspect.getmembers(cls):
178
- # remove private and protected functions
179
- if not member[0].startswith("_"):
180
- # remove other methods and functions
181
- if (
182
- not inspect.ismethod(member[1])
183
- and not inspect.isfunction(member[1])
184
- and not inspect.isclass(member[1])
185
- ):
186
- if member[0] not in (
187
- "INFO_URL",
188
- "ICON_URL",
189
- "PERSISTENT_PROPERTY_LIST",
190
- "log_to_console",
191
- "logger",
192
- "iris_handle",
193
- "DISPATCH",
194
- "adapter",
195
- "Adapter",
196
- "buffer",
197
- "BusinessHost",
198
- "business_host",
199
- "business_host_python",
200
- ):
201
- name = member[0]
202
- req = 0
203
- cat = "Additional"
204
- desc = ""
205
- # get value, set to "" if None or a @property
206
- val = member[1]
207
- if isinstance(val, property) or (val is None):
208
- val = ""
209
- dt = str(type(val))[8:-2]
210
- # get datatype from attribute definition, default to String
211
- data_type = {
212
- "int": "Integer",
213
- "float": "Numeric",
214
- "complex": "Numeric",
215
- "bool": "Boolean",
216
- }.get(dt, "String")
217
- # if the user has created a attr_info function, then check the annotation on the return from that for more information about this attribute
218
- if hasattr(cls, name + "_info"):
219
- func = getattr(cls, name + "_info")
220
- if callable(func):
221
- annotations = func.__annotations__["return"]
222
- if annotations is not None:
223
- if bool(annotations.get("ExcludeFromSettings")):
224
- # don't add this attribute to the settings list
225
- continue
226
- req = bool(annotations.get("IsRequired"))
227
- cat = annotations.get("Category", "Additional")
228
- desc = annotations.get("Description")
229
- dt = annotations.get("DataType")
230
- # only override DataType found
231
- if (dt is not None) and ("" != dt):
232
- data_type = {
233
- int: "Integer",
234
- float: "Number",
235
- complex: "Number",
236
- bool: "Boolean",
237
- str: "String",
238
- }.get(dt, str(dt))
239
- default = func()
240
- if default is not None:
241
- val = default
242
- # create list of information for this specific property
243
- info = []
244
- info.append(name) # Name
245
- info.append(data_type) # DataType
246
- info.append(val) # Default Value
247
- info.append(req) # Required
248
- info.append(cat) # Category
249
- info.append(desc) # Description
250
- # add this property to the list
251
- ret.append(info)
309
+ annotations = _type_hints_with_extras(cls)
310
+ members = dict(inspect.getmembers(cls))
311
+
312
+ names = [
313
+ name
314
+ for name, value in members.items()
315
+ if _is_setting_member(name, value)
316
+ ]
317
+ for name in annotations:
318
+ if (
319
+ name in names
320
+ or name.startswith("_")
321
+ or name in _EXCLUDED_SETTING_NAMES
322
+ ):
323
+ continue
324
+ value = members.get(name, _NO_VALUE)
325
+ if value is not _NO_VALUE and not _is_setting_member(name, value):
326
+ continue
327
+ names.append(name)
328
+
329
+ for name in names:
330
+ member_exists = name in members
331
+ val = members.get(name, "")
332
+ annotated_type, annotated_setting = _setting_from_annotation(
333
+ annotations.get(name)
334
+ )
335
+ value_setting = val if isinstance(val, Setting) else None
336
+ setting_info = value_setting or annotated_setting
337
+
338
+ if setting_info is not None and setting_info.exclude:
339
+ continue
340
+
341
+ req = bool(setting_info.required) if setting_info is not None else False
342
+ cat = setting_info.category if setting_info is not None else ""
343
+ desc = setting_info.description if setting_info is not None else ""
344
+ control = setting_info.control if setting_info is not None else ""
345
+
346
+ if value_setting is not None:
347
+ val = value_setting.default if value_setting.has_default else ""
348
+ elif (
349
+ not member_exists
350
+ and annotated_setting is not None
351
+ and annotated_setting.has_default
352
+ ):
353
+ val = annotated_setting.default
354
+ elif not member_exists:
355
+ val = ""
356
+
357
+ if isinstance(val, property) or (val is None):
358
+ val = ""
359
+
360
+ if setting_info is not None and setting_info.iris_type:
361
+ data_type = setting_info.iris_type
362
+ else:
363
+ data_type_source = (
364
+ setting_info.data_type
365
+ if setting_info is not None and setting_info.data_type
366
+ else annotated_type
367
+ )
368
+ data_type = _iris_data_type(data_type_source)
369
+ if data_type is None:
370
+ data_type = _iris_data_type(type(val)) or "String"
371
+
372
+ # Legacy attr_info() support. Values supplied here keep working
373
+ # and can also provide the new control/editor context field.
374
+ if hasattr(cls, name + "_info"):
375
+ func = getattr(cls, name + "_info")
376
+ if callable(func):
377
+ info_annotations = getattr(func, "__annotations__", {}).get(
378
+ "return"
379
+ )
380
+ if info_annotations is not None:
381
+ if bool(info_annotations.get("ExcludeFromSettings")):
382
+ continue
383
+ req = bool(info_annotations.get("IsRequired", req))
384
+ cat = _string_metadata(
385
+ info_annotations.get("Category", cat)
386
+ )
387
+ desc = _string_metadata(
388
+ info_annotations.get("Description", desc)
389
+ )
390
+ control = _string_metadata(
391
+ info_annotations.get(
392
+ "Control",
393
+ info_annotations.get("EditorContext", control),
394
+ )
395
+ )
396
+ dt = info_annotations.get("DataType")
397
+ if (dt is not None) and ("" != dt):
398
+ data_type = _iris_data_type(dt) or str(dt)
399
+ default = func()
400
+ if default is not None:
401
+ val = default
402
+
403
+ ret.append(
404
+ [
405
+ name, # Name
406
+ data_type, # DataType
407
+ val, # Default Value
408
+ req, # Required
409
+ _string_metadata(cat), # Category
410
+ _string_metadata(desc), # Description
411
+ _string_metadata(control), # Control/editor context
412
+ ]
413
+ )
252
414
  except Exception:
253
415
  pass
254
416
  return ret
iop/_local.py CHANGED
@@ -95,7 +95,7 @@ class _LocalDirector(_DirectorProtocol):
95
95
  # ------------------------------------------------------------------
96
96
 
97
97
  def migrate(self, path: str) -> None:
98
- _Utils.migrate(path)
98
+ _Utils.migrate(path, mode="LOCAL", namespace=self.namespace)
99
99
 
100
100
  # ------------------------------------------------------------------
101
101
  # Metadata
@@ -0,0 +1,6 @@
1
+ class _PollingBusinessServiceMixin:
2
+ """Mixin for services polled by the default IRIS inbound adapter."""
3
+
4
+ @staticmethod
5
+ def get_adapter_type() -> str:
6
+ return "Ens.InboundAdapter"
iop/_settings.py ADDED
@@ -0,0 +1,188 @@
1
+ from enum import Enum
2
+ from typing import Any
3
+
4
+
5
+ _MISSING = object()
6
+
7
+
8
+ class Category(str, Enum):
9
+ """Common IRIS production setting categories for Management Portal grouping."""
10
+
11
+ INFO = "Info"
12
+ BASIC = "Basic"
13
+ CONNECTION = "Connection"
14
+ ADDITIONAL = "Additional"
15
+ ALERTING = "Alerting"
16
+ DEV = "Dev"
17
+
18
+
19
+ def _string_value(value: Any) -> str:
20
+ if isinstance(value, Enum):
21
+ return str(value.value)
22
+ return str(value)
23
+
24
+
25
+ class Setting:
26
+ """Metadata for an IRIS production setting."""
27
+
28
+ def __init__(
29
+ self,
30
+ default: Any = _MISSING,
31
+ *,
32
+ data_type: Any = None,
33
+ iris_type: str | None = None,
34
+ category: str | Category | None = None,
35
+ required: bool = False,
36
+ description: str = "",
37
+ control: str | None = None,
38
+ exclude: bool = False,
39
+ ):
40
+ self.default = default
41
+ self.data_type = data_type
42
+ self.iris_type = iris_type
43
+ self.category = _string_value(category) if category is not None else None
44
+ self.required = required
45
+ self.description = description or ""
46
+ self.control = control or ""
47
+ self.exclude = exclude
48
+ self.name = ""
49
+
50
+ def __set_name__(self, owner, name):
51
+ self.name = name
52
+
53
+ def __get__(self, instance, owner=None):
54
+ if instance is None:
55
+ return self
56
+ if self.name in instance.__dict__:
57
+ return instance.__dict__[self.name]
58
+ if self.default is _MISSING:
59
+ return None
60
+ return self.default
61
+
62
+ def __set__(self, instance, value):
63
+ instance.__dict__[self.name] = value
64
+
65
+ @property
66
+ def has_default(self) -> bool:
67
+ return self.default is not _MISSING
68
+
69
+
70
+ def setting(default: Any = _MISSING, **kwargs) -> Setting:
71
+ return Setting(default, **kwargs)
72
+
73
+
74
+ class _Controls:
75
+ """Helpers for IRIS production setting editor controls."""
76
+
77
+ @staticmethod
78
+ def raw(value: str) -> str:
79
+ """Return an advanced IRIS editor context string unchanged."""
80
+ return value
81
+
82
+ @staticmethod
83
+ def selector(
84
+ context: str | None = None, *, multi_select: bool | None = None, **params
85
+ ) -> str:
86
+ """Build an IRIS selector editor context from a context search string."""
87
+ query = []
88
+ if multi_select is not None:
89
+ query.append(f"multiSelect={1 if multi_select else 0}")
90
+ if context:
91
+ if not (context.startswith("{") and context.endswith("}")):
92
+ context = "{" + context + "}"
93
+ query.append(f"context={context}")
94
+ for key, value in params.items():
95
+ if value is not None:
96
+ query.append(f"{key}={value}")
97
+ return "selector" + (f"?{'&'.join(query)}" if query else "")
98
+
99
+ @staticmethod
100
+ def production_item(
101
+ *,
102
+ targets: bool = True,
103
+ production_name: str = "@productionId",
104
+ multi_select: bool = False,
105
+ ) -> str:
106
+ """Select production items, normally target components, from a production."""
107
+ context = (
108
+ "Ens.ContextSearch/ProductionItems?"
109
+ f"targets={1 if targets else 0}&productionName={production_name}"
110
+ )
111
+ return _Controls.selector(context, multi_select=multi_select or None)
112
+
113
+ @staticmethod
114
+ def partner() -> str:
115
+ """Select a partner setting value."""
116
+ return "partnerSelector"
117
+
118
+ @staticmethod
119
+ def rule() -> str:
120
+ """Select an IRIS rule definition."""
121
+ return "ruleSelector"
122
+
123
+ @staticmethod
124
+ def credentials() -> str:
125
+ """Select an IRIS credentials entry."""
126
+ return "credentialsSelector"
127
+
128
+ @staticmethod
129
+ def directory() -> str:
130
+ """Select a directory path."""
131
+ return "directorySelector"
132
+
133
+ @staticmethod
134
+ def file() -> str:
135
+ """Select a file path."""
136
+ return "fileSelector"
137
+
138
+ @staticmethod
139
+ def dtl() -> str:
140
+ """Select an IRIS DTL data transformation."""
141
+ return "dtlSelector"
142
+
143
+ @staticmethod
144
+ def schedule() -> str:
145
+ """Select an IRIS schedule."""
146
+ return "scheduleSelector"
147
+
148
+ @staticmethod
149
+ def ssl_config() -> str:
150
+ """Select an IRIS SSL/TLS configuration."""
151
+ return "sslConfigSelector"
152
+
153
+ @staticmethod
154
+ def bpl() -> str:
155
+ """Select an IRIS BPL business process."""
156
+ return "bplSelector"
157
+
158
+ @staticmethod
159
+ def character_set() -> str:
160
+ """Select an IRIS character set."""
161
+ return _Controls.selector("Ens.ContextSearch/CharacterSets")
162
+
163
+ charset = character_set
164
+
165
+ @staticmethod
166
+ def framing(*, host: str = "@currHostId", prop: str = "Framing") -> str:
167
+ """Select a display-list value such as the current host framing option."""
168
+ return _Controls.selector(
169
+ f"Ens.ContextSearch/getDisplayList?host={host}&prop={prop}"
170
+ )
171
+
172
+ @staticmethod
173
+ def local_interface() -> str:
174
+ """Select a configured TCP local interface."""
175
+ return _Controls.selector("Ens.ContextSearch/TCPLocalInterfaces")
176
+
177
+ @staticmethod
178
+ def schema_category(host: str) -> str:
179
+ """Select a schema category for the specified host expression."""
180
+ return _Controls.selector(f"Ens.ContextSearch/SchemaCategories?host={host}")
181
+
182
+ @staticmethod
183
+ def search_table(host: str) -> str:
184
+ """Select a search table class for the specified host expression."""
185
+ return _Controls.selector(f"Ens.ContextSearch/SearchTableClasses?host={host}")
186
+
187
+
188
+ controls = _Controls()
iop/_utils.py CHANGED
@@ -6,7 +6,7 @@ import importlib.resources
6
6
  import json
7
7
  import inspect
8
8
  import ast
9
- from typing import Optional, Tuple
9
+ from typing import Any, Optional, Tuple
10
10
 
11
11
  import xmltodict
12
12
  from pydantic import TypeAdapter
@@ -71,6 +71,13 @@ class _Utils:
71
71
 
72
72
  :param cls: The class to register
73
73
  """
74
+ if is_persistent_message_class(msg_cls):
75
+ raise ValueError(
76
+ f"{_Utils._python_classname(msg_cls)} is a PersistentMessage. "
77
+ "Register it in CLASSES, not SCHEMAS."
78
+ )
79
+ if not inspect.isclass(msg_cls):
80
+ raise ValueError("SCHEMAS entries must be message classes.")
74
81
  if issubclass(msg_cls, _PydanticMessage):
75
82
  schema = msg_cls.model_json_schema()
76
83
  elif issubclass(msg_cls, _Message):
@@ -78,7 +85,8 @@ class _Utils:
78
85
  schema = type_adapter.json_schema()
79
86
  else:
80
87
  raise ValueError(
81
- "The class must be a subclass of _Message or _PydanticMessage"
88
+ f"{_Utils._python_classname(msg_cls)} cannot be registered as a "
89
+ "DTL schema. Use a Message or PydanticMessage subclass."
82
90
  )
83
91
  schema_name = msg_cls.__module__ + "." + msg_cls.__name__
84
92
  schema_str = json.dumps(schema)
@@ -118,6 +126,23 @@ class _Utils:
118
126
  msg_cls, iris_classname, sync_schema=sync_schema
119
127
  )
120
128
 
129
+ @staticmethod
130
+ def _python_classname(value: Any) -> str:
131
+ module = getattr(value, "__module__", "")
132
+ name = getattr(value, "__name__", repr(value))
133
+ return f"{module}.{name}" if module else str(name)
134
+
135
+ @staticmethod
136
+ def _is_message_schema_class(value: Any) -> bool:
137
+ try:
138
+ return (
139
+ inspect.isclass(value)
140
+ and (issubclass(value, _Message) or issubclass(value, _PydanticMessage))
141
+ and not is_persistent_message_class(value)
142
+ )
143
+ except TypeError:
144
+ return False
145
+
121
146
  @staticmethod
122
147
  def get_python_settings() -> Tuple[str, str, str]:
123
148
  import iris_utils._cli
@@ -268,6 +293,7 @@ class _Utils:
268
293
  "BusinessOperation",
269
294
  "BusinessProcess",
270
295
  "BusinessService",
296
+ "PollingBusinessService",
271
297
  "DuplexService",
272
298
  "DuplexProcess",
273
299
  "DuplexOperation",
@@ -329,7 +355,9 @@ class _Utils:
329
355
  return module
330
356
 
331
357
  @staticmethod
332
- def migrate(filename=None):
358
+ def migrate(
359
+ filename=None, mode: Optional[str] = None, namespace: Optional[str] = None
360
+ ):
333
361
  """
334
362
  Read the settings.py file and register all the components
335
363
  settings.py file has two dictionaries:
@@ -345,9 +373,120 @@ class _Utils:
345
373
  """
346
374
  settings, path = _Utils._load_settings(filename)
347
375
 
348
- _Utils._register_settings_components(settings, path)
376
+ try:
377
+ plan = _Utils._build_migration_plan(
378
+ settings, path, filename, mode=mode, namespace=namespace
379
+ )
380
+ print(_Utils.format_migration_plan(plan))
381
+ _Utils._register_settings_components(settings, path)
382
+ print(
383
+ _Utils.format_migration_success(
384
+ filename or inspect.getfile(settings), namespace=namespace
385
+ )
386
+ )
387
+ finally:
388
+ _Utils._cleanup_sys_path(path)
389
+
390
+ @staticmethod
391
+ def explain_migration(
392
+ filename=None, mode: Optional[str] = None, namespace: Optional[str] = None
393
+ ):
394
+ """Return a human-readable migration plan without writing to IRIS."""
395
+ settings, path = _Utils._load_settings(filename)
396
+ try:
397
+ plan = _Utils._build_migration_plan(
398
+ settings, path, filename, mode=mode, namespace=namespace
399
+ )
400
+ return _Utils.format_migration_plan(plan)
401
+ finally:
402
+ _Utils._cleanup_sys_path(path)
403
+
404
+ @staticmethod
405
+ def format_migration_success(filename, namespace: Optional[str] = None):
406
+ suffix = f" in namespace {namespace}" if namespace else ""
407
+ return f"Migration succeeded{suffix}: {filename}"
349
408
 
350
- _Utils._cleanup_sys_path(path)
409
+ @staticmethod
410
+ def format_migration_plan(plan):
411
+ """Format a migration plan for CLI and migration output."""
412
+ lines = [f"Migration plan: {plan['settings']}"]
413
+ if plan.get("mode"):
414
+ lines.append(f"Mode: {plan['mode']}")
415
+ if plan.get("namespace"):
416
+ lines.append(f"Namespace: {plan['namespace']}")
417
+ lines.append("")
418
+ lines.extend(_Utils._format_plan_section("CLASSES", plan["classes"]))
419
+ lines.extend(_Utils._format_plan_section("SCHEMAS", plan["schemas"]))
420
+ lines.extend(_Utils._format_plan_section("PRODUCTIONS", plan["productions"]))
421
+ return "\n".join(lines)
422
+
423
+ @staticmethod
424
+ def _format_plan_section(title, entries):
425
+ lines = [f"{title}:"]
426
+ if entries:
427
+ lines.extend(f" {entry}" for entry in entries)
428
+ else:
429
+ lines.append(" none")
430
+ lines.append("")
431
+ return lines
432
+
433
+ @staticmethod
434
+ def _build_migration_plan(
435
+ settings,
436
+ path,
437
+ filename=None,
438
+ mode: Optional[str] = None,
439
+ namespace: Optional[str] = None,
440
+ ):
441
+ """Build and validate a migration plan from a settings module."""
442
+ if not path:
443
+ path = os.path.dirname(inspect.getfile(settings))
444
+
445
+ plan = {
446
+ "settings": filename or inspect.getfile(settings),
447
+ "mode": mode,
448
+ "namespace": namespace,
449
+ "classes": [],
450
+ "schemas": [],
451
+ "productions": [],
452
+ }
453
+
454
+ classes = getattr(settings, "CLASSES", {})
455
+ if not isinstance(classes, dict):
456
+ raise ValueError("CLASSES must be a dictionary.")
457
+ for key, value in classes.items():
458
+ kind, target = _Utils._classify_class_setting(value, path)
459
+ if kind == "message_schema":
460
+ schema_hint = value.__name__ if inspect.isclass(value) else target
461
+ raise ValueError(
462
+ f"{target} is a Message/PydanticMessage and cannot be registered "
463
+ f"in CLASSES. Use SCHEMAS = [{schema_hint}] if you "
464
+ "need DTL support. Otherwise, no migration is required for this "
465
+ "message."
466
+ )
467
+ if kind == "persistent_message":
468
+ plan["classes"].append(f"{key} -> {target} (PersistentMessage)")
469
+ else:
470
+ plan["classes"].append(f"{key} -> {target} (component)")
471
+
472
+ schemas = getattr(settings, "SCHEMAS", None)
473
+ if schemas is not None:
474
+ if not isinstance(schemas, list):
475
+ raise ValueError("SCHEMAS must be a list of message classes.")
476
+ for cls in schemas:
477
+ _Utils._validate_dtl_schema_class(cls, "SCHEMAS")
478
+ plan["schemas"].append(_Utils._python_classname(cls))
479
+
480
+ productions = getattr(settings, "PRODUCTIONS", None)
481
+ if productions is not None:
482
+ if not isinstance(productions, list):
483
+ raise ValueError("PRODUCTIONS must be a list.")
484
+ for production in productions:
485
+ if not isinstance(production, dict) or not production:
486
+ raise ValueError("Each PRODUCTION entry must be a non-empty dict.")
487
+ plan["productions"].append(next(iter(production.keys())))
488
+
489
+ return plan
351
490
 
352
491
  @staticmethod
353
492
  def _load_settings(filename):
@@ -390,6 +529,52 @@ class _Utils:
390
529
  else:
391
530
  return os.getcwd()
392
531
 
532
+ @staticmethod
533
+ def _classify_class_setting(value, root_path=None):
534
+ if inspect.isclass(value):
535
+ if is_persistent_message_class(value):
536
+ return "persistent_message", _Utils._python_classname(value)
537
+ if _Utils._is_message_schema_class(value):
538
+ return "message_schema", _Utils._python_classname(value)
539
+ return "component", _Utils._python_classname(value)
540
+
541
+ if inspect.ismodule(value):
542
+ return "component", f"{value.__name__}.*"
543
+
544
+ if isinstance(value, dict):
545
+ if "path" in value and "module" in value and "class" in value:
546
+ cls = _Utils._try_import_class(
547
+ value["module"], value["class"], value["path"]
548
+ )
549
+ target = f"{value['module']}.{value['class']}"
550
+ if cls is not None:
551
+ if is_persistent_message_class(cls):
552
+ return "persistent_message", target
553
+ if _Utils._is_message_schema_class(cls):
554
+ return "message_schema", target
555
+ return "component", target
556
+ if "path" in value and "package" in value:
557
+ return "component", f"{value['package']} package"
558
+ if "path" in value and "file" in value:
559
+ return "component", value["file"]
560
+ if "path" in value:
561
+ return "component", value["path"]
562
+
563
+ raise ValueError(f"Invalid migration class entry: {value!r}.")
564
+
565
+ @staticmethod
566
+ def _validate_dtl_schema_class(cls, setting_name):
567
+ if is_persistent_message_class(cls):
568
+ raise ValueError(
569
+ f"{_Utils._python_classname(cls)} is a PersistentMessage. Register it "
570
+ "in CLASSES, not SCHEMAS."
571
+ )
572
+ if not _Utils._is_message_schema_class(cls):
573
+ raise ValueError(
574
+ f"{_Utils._python_classname(cls)} cannot be registered in "
575
+ f"{setting_name}. Use a Message or PydanticMessage subclass."
576
+ )
577
+
393
578
  @staticmethod
394
579
  def _register_settings_components(settings, path):
395
580
  """Register all components from settings (classes, productions, schemas).
@@ -402,24 +587,23 @@ class _Utils:
402
587
  if not path:
403
588
  path = os.path.dirname(inspect.getfile(settings))
404
589
 
405
- try:
406
- # set the classes settings
407
- _Utils.set_classes_settings(settings.CLASSES, path)
408
- except AttributeError:
409
- print("No classes to register")
590
+ class_items = getattr(settings, "CLASSES", None)
591
+ if class_items is not None:
592
+ _Utils.set_classes_settings(class_items, path)
410
593
 
411
594
  try:
412
595
  # set the productions settings
413
596
  _Utils.set_productions_settings(settings.PRODUCTIONS, path)
414
597
  except AttributeError:
415
- print("No productions to register")
416
-
417
- try:
418
- # set the schemas
419
- for cls in settings.SCHEMAS:
598
+ pass
599
+
600
+ schemas = getattr(settings, "SCHEMAS", None)
601
+ if schemas is not None:
602
+ if not isinstance(schemas, list):
603
+ raise ValueError("SCHEMAS must be a list of message classes.")
604
+ for cls in schemas:
605
+ _Utils._validate_dtl_schema_class(cls, "SCHEMAS")
420
606
  _Utils.register_message_schema(cls)
421
- except AttributeError:
422
- print("No schemas to register")
423
607
 
424
608
  @staticmethod
425
609
  def _cleanup_sys_path(path):
@@ -456,11 +640,21 @@ class _Utils:
456
640
  :param class_items: a dictionary of classes
457
641
  :return: a dictionary of settings for each class
458
642
  """
643
+ if not isinstance(class_items, dict):
644
+ raise ValueError("CLASSES must be a dictionary.")
459
645
  for key, value in class_items.items():
460
646
  if inspect.isclass(value):
461
647
  if is_persistent_message_class(value):
462
648
  _Utils.register_persistent_message(value, key)
463
649
  continue
650
+ if _Utils._is_message_schema_class(value):
651
+ raise ValueError(
652
+ f"{_Utils._python_classname(value)} is a Message/"
653
+ "PydanticMessage and cannot be registered in CLASSES. "
654
+ f"Use SCHEMAS = [{value.__name__}] if you need DTL "
655
+ "support. Otherwise, no migration is required for this "
656
+ "message."
657
+ )
464
658
  path = None
465
659
  if root_path:
466
660
  path = root_path
@@ -486,6 +680,14 @@ class _Utils:
486
680
  if msg_cls is not None and is_persistent_message_class(msg_cls):
487
681
  _Utils.register_persistent_message(msg_cls, key)
488
682
  continue
683
+ if msg_cls is not None and _Utils._is_message_schema_class(msg_cls):
684
+ raise ValueError(
685
+ f"{value['module']}.{value['class']} is a Message/"
686
+ "PydanticMessage and cannot be registered in CLASSES. "
687
+ "Use SCHEMAS if you need DTL "
688
+ "support. Otherwise, no migration is required for this "
689
+ "message."
690
+ )
489
691
  # register the component
490
692
  _Utils.register_component(
491
693
  value["module"], value["class"], value["path"], 1, key
iop/cls/IOP/Utils.cls CHANGED
@@ -352,12 +352,32 @@ ClassMethod GenerateProxyClass(
352
352
  }
353
353
  }
354
354
  Set tCustomProp.Required = tPropInfo."__getitem__"(3)
355
+ Set tDesc = ""
356
+ If (builtins.len(tPropInfo)>5) {
357
+ Set tDesc = tPropInfo."__getitem__"(5)
358
+ }
359
+ If ""'=tDesc {
360
+ Set tCustomProp.Description = tDesc
361
+ }
355
362
 
356
363
  Set tSC = tCOSClass.Properties.Insert(tCustomProp)
357
364
  Quit:$$$ISERR(tSC)
358
365
 
359
- Set tPropCat = "Python Attributes $type"
366
+ Set tPropCat = ""
367
+ If (builtins.len(tPropInfo)>4) {
368
+ Set tPropCat = tPropInfo."__getitem__"(4)
369
+ }
370
+ If ""=tPropCat {
371
+ Set tPropCat = "Python Attributes $type"
372
+ }
373
+ Set tContext = ""
374
+ If (builtins.len(tPropInfo)>6) {
375
+ Set tContext = tPropInfo."__getitem__"(6)
376
+ }
360
377
  Set tSETTINGSParamValue = tSETTINGSParamValue_","_tPropName_":"_tPropCat
378
+ If ""'=tContext {
379
+ Set tSETTINGSParamValue = tSETTINGSParamValue_":"_tContext
380
+ }
361
381
  }
362
382
  Quit:$$$ISERR(tSC)
363
383
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iris_pex_embedded_python
3
- Version: 3.7.2b1
3
+ Version: 3.7.2b2
4
4
  Summary: Iris Interoperability based on Embedded Python
5
5
  Author-email: grongier <guillaume.rongier@intersystems.com>
6
6
  License: MIT License
@@ -19,7 +19,7 @@ grongier/cls/Grongier/PEX/PrivateSession/Message/Poll.cls,sha256=pcUgHgxX1pMH-wQ
19
19
  grongier/cls/Grongier/PEX/PrivateSession/Message/Start.cls,sha256=T3jNoR8RjKr1InQ6SgqBYTgFwpSB0Q60WholjbvForg,433
20
20
  grongier/cls/Grongier/PEX/PrivateSession/Message/Stop.cls,sha256=zy30ZXXN4XcovPij-kOF3PuH1SkP1EUvlEJQRx2S9RU,431
21
21
  grongier/cls/Grongier/Service/WSGI.cls,sha256=7u2SsFmnsubMfdazvaDchKCM3yesPRMfKBzMIkwQ9xc,77
22
- grongier/pex/__init__.py,sha256=mOCWppLEgwnLHzY_8UFFbT2ftZ22KApvDOjhYRB4SQ8,1355
22
+ grongier/pex/__init__.py,sha256=XxMw6cAaCEEtmFIAL5-FrRkM1u-aQbbYvzr1WE7zYMw,1698
23
23
  grongier/pex/__main__.py,sha256=pQzVtkDhAeI6dpNRC632dVk2SGZZIEDwDufdgZe8VWs,98
24
24
  grongier/pex/_business_host.py,sha256=dlV8CWJad8Pr2TNfD9OjcVKaq5gEYQACZla1FK6-bDM,44
25
25
  grongier/pex/_cli.py,sha256=hOHz3n-aHtULuhdCkqZ_SSb3sv7M6j2WhRxgCTvgR9I,64
@@ -27,15 +27,15 @@ grongier/pex/_common.py,sha256=HZwG2C2-yB8yNN8kXhI6vxg8h-rROuEx38YOVFWIk1s,31
27
27
  grongier/pex/_director.py,sha256=pCmoiJ-sxe24yQaDz6ZFBsAnqU6fh57_dlew98B7rtE,35
28
28
  grongier/pex/_utils.py,sha256=gvsdr8WhWrE6smlsCxhoF14VUZfitrwqr5J57HkJhi4,29
29
29
  grongier/pex/wsgi/handlers.py,sha256=NrFLo_YbAh-x_PlWhAiWkQnUUN2Ss9HoEm63dDWCBpQ,2947
30
- iop/__init__.py,sha256=rtIVTkzR8gxm2zZBQZBkYO8bWylkce0TiUKslwCw7IQ,1510
30
+ iop/__init__.py,sha256=tb9Vcz2XzApqKWxit4WTEecyiPB9PRWG8MJdPN9J_wI,1853
31
31
  iop/__main__.py,sha256=C0MZpXA8XFopuveMDbZgX1bttrb2DxarhaY1XYfbhsg,100
32
32
  iop/_async_request.py,sha256=lW3vasdepKwLPuWRm1zDs3xlRWCoVL0RJZqVUj8gkJw,2409
33
33
  iop/_business_host.py,sha256=l0X_2xiMArMQsvI99kXDuI6vVrJMvPHZtDnXHGqT7c4,11669
34
34
  iop/_business_operation.py,sha256=AhKw-KKpOatXGqaMFwMcb774SF0a4ZIWeRCUGN-al_Q,2985
35
35
  iop/_business_process.py,sha256=tDgZSb8JhwzUAbBXvm9qKzbdlhaeXfVLoUIGOXCoeZs,8821
36
36
  iop/_business_service.py,sha256=3oSL4_Dam_WDD7J2BpYVd_WQKUPUnCcLH_bjwXvWqKc,3960
37
- iop/_cli.py,sha256=h_s2QZpl0DpeNB6228y3LJEeZDwfvDdJcKSQLkRRVzA,16030
38
- iop/_common.py,sha256=Vo_lrXNm0u_3naDku_Ma8gjBqN0-kpOIUR2vfBOcrrM,15772
37
+ iop/_cli.py,sha256=O9VMWBZnuubA5CZmvp7rMJfcBfQPi7FhNkxHBs7ncXk,17072
38
+ iop/_common.py,sha256=U10pvB1sSFhtJfJ7BzEo19XfNWAUDv54dqFFqp1Gl0g,19234
39
39
  iop/_debugpy.py,sha256=0UOLkOjXkJ24GxxyEnPckafrAtfVVUDYIedmLIznYfM,6067
40
40
  iop/_decorators.py,sha256=2UFV56sfopuBqgxTTj71XgAAwoQm1ijDi_3N-GIKbu8,2337
41
41
  iop/_director.py,sha256=dN_SGyUDPH19kUt7_AR6vBrDlKUg0RRgYVkvJFjsYOM,11855
@@ -44,17 +44,19 @@ iop/_dispatch.py,sha256=UjrQuia4lSgC3It07m9BqnEBhmqjdd9Hp_b2QVaj4zc,5825
44
44
  iop/_generator_request.py,sha256=2No4E2mjBr7bPwBljsKbWX3QxTrjgdyz683tynlg-OI,1339
45
45
  iop/_inbound_adapter.py,sha256=WErDcXzHhYNN0QanmTay34Zh_uUxlEykJ4YmevTVOyY,1654
46
46
  iop/_iris.py,sha256=Zc4IvMv3us3rrBNCba1AU8RSXvct2wZkCZVSWN84c-k,198
47
- iop/_local.py,sha256=7N-xEjVRQmP6h1fEHQBlsPKBBe2B90KbLqi53cvA7Yo,3636
47
+ iop/_local.py,sha256=bGTxgI83qsT5IxA-hZXhXEDAICeYc3Lku2MM5A8ApBs,3676
48
48
  iop/_log_manager.py,sha256=Egrul1LCQlYrvIgcoF-LjXoeXaLLh_nD9bB4P-Y-q_E,3225
49
49
  iop/_message.py,sha256=fTHxYHBq42QIDGrtVuw-7NJAQ7bLIWPdbl5EmwiDhG0,1465
50
50
  iop/_message_validator.py,sha256=XzQ4qkHLYoai2_gPk3ItXHVgwz2WldiBcNE32yaxOVc,1540
51
51
  iop/_outbound_adapter.py,sha256=GSsr6z0gvVygNyr3YicHBt7i2ZJu3wJgrCpPqod7Z_U,732
52
52
  iop/_persistent_message.py,sha256=F1WsgNqSupyKPkldQNnY96cGtD2mRwR9qSVE8SI6nXU,16780
53
+ iop/_polling_business_service.py,sha256=uElSd77H7YhE3hi24Zzyfc9BMP8HSzGyo4fRzW3e0eM,199
53
54
  iop/_private_session_duplex.py,sha256=SLqwnB4gLKqCt-DClIEFg_yWcQs16A4Va7lRyUg-18o,5289
54
55
  iop/_private_session_process.py,sha256=J1HjRIphg0iHHEIa6Rqa68vA22ODMN6BfNqAN4Ybi1U,1723
55
56
  iop/_remote.py,sha256=zE4eZtNuqQemtX2gBwu59n56ixBtwkV25GvZgqVKlMU,17395
56
57
  iop/_serialization.py,sha256=5ajXh99QVFRXuwhR7xex3rHF87l07o8Fth2VOMRj-YM,8778
57
- iop/_utils.py,sha256=qBV546JXjWFO_KWB0n4OcKTuaiNcDvD3IJt9jh0a_Oo,26615
58
+ iop/_settings.py,sha256=ZoPbYllsJJ5uAwcg1z2Gp-PChOAnKwMLsFNXppBuUGM,5510
59
+ iop/_utils.py,sha256=46EO1ugcylkd4oLodvTyp2-byutgePjITHv0fXUjscI,35460
58
60
  iop/cls/IOP/BusinessOperation.cls,sha256=NlvbNtm1ZFZmHaMX_9FMKoptY-hQMq5jYN1nLQwvYJw,936
59
61
  iop/cls/IOP/BusinessProcess.cls,sha256=XJxzbiV0xokzRm-iI2Be5UIJLE3MlXr7W3WS_LkOCYs,3363
60
62
  iop/cls/IOP/BusinessService.cls,sha256=fplKrbQgA7cQgjKIqDR2IK2iD1iNHmT-QvWrozhE4n4,1189
@@ -66,7 +68,7 @@ iop/cls/IOP/OutboundAdapter.cls,sha256=OQoGFHUy2qV_kcsShTlWGOngDrdH5dhwux4eopZyI
66
68
  iop/cls/IOP/PickleMessage.cls,sha256=S3y7AClQ8mAILjxPuHdCjGosBZYzGbUQ5WTv4mYPNMQ,1673
67
69
  iop/cls/IOP/Projection.cls,sha256=AZgbfpbEk02llhyIwrSw0M3QMcQNcjhjY3_vU_yx8FU,1315
68
70
  iop/cls/IOP/Test.cls,sha256=zvlCZJfOCmSFdmi-ZQHWDGYmqZAgRspaJj1j0r_IxJQ,2017
69
- iop/cls/IOP/Utils.cls,sha256=NGnfi2Kif3OyYwR6pm5c_-UKm5vEwQyfzvJpZGxFeeA,18546
71
+ iop/cls/IOP/Utils.cls,sha256=1ujVAaBAjFAbbyirD0aFzRQoQva6SR_qJJG_s03V1l8,19042
70
72
  iop/cls/IOP/Wrapper.cls,sha256=37fUol-EcktdfGhpfi4o12p04975lKGaRYEFhw-fuaM,1614
71
73
  iop/cls/IOP/Duplex/Operation.cls,sha256=K_fmgeLjPZQbHgNrc0kd6DUQoW0fDn1VHQjJxHo95Zk,525
72
74
  iop/cls/IOP/Duplex/Process.cls,sha256=xbefZ4z84a_IUhavWN6P_gZBzqkdJ5XRTXxro6iDvAg,6986
@@ -86,9 +88,9 @@ iop/cls/IOP/Service/WSGI.cls,sha256=VLNCXEwmHW9dBnE51uGE1nvGX6T4HjhqePT3LVhsjAE,
86
88
  iop/cls/IOP/Service/Remote/Handler.cls,sha256=JfsXse2jvoVvQfW8_rVEt2DCQJ9SVqReCcOUngOkpzE,938
87
89
  iop/cls/IOP/Service/Remote/Rest/v1.cls,sha256=DRaYNsbFmczdVltb-HMZvviy5EcjcJx7__zmnKANNm8,14429
88
90
  iop/wsgi/handlers.py,sha256=lcCZ1ixnYqdo9eNrBSsQbhS9ei2x11p9e4SHHb__zmY,2951
89
- iris_pex_embedded_python-3.7.2b1.dist-info/licenses/LICENSE,sha256=rZSiBFId_sfbJ6RL0GjjPX-InNLkNS9ou7eQsikciI8,1089
90
- iris_pex_embedded_python-3.7.2b1.dist-info/METADATA,sha256=VNHpmk3bjgjjn-O8AU-7jwLP1vmvPajAYvoVfBo-8Sk,4310
91
- iris_pex_embedded_python-3.7.2b1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
92
- iris_pex_embedded_python-3.7.2b1.dist-info/entry_points.txt,sha256=pj-i4LSDyiSP6xpHlVjMCbg1Pik7dC3_sdGY3Yp9Vhk,38
93
- iris_pex_embedded_python-3.7.2b1.dist-info/top_level.txt,sha256=4p0q6hCATmYIVMVi3I8hOUcJE1kwzyBeHygWv_rGvrU,13
94
- iris_pex_embedded_python-3.7.2b1.dist-info/RECORD,,
91
+ iris_pex_embedded_python-3.7.2b2.dist-info/licenses/LICENSE,sha256=rZSiBFId_sfbJ6RL0GjjPX-InNLkNS9ou7eQsikciI8,1089
92
+ iris_pex_embedded_python-3.7.2b2.dist-info/METADATA,sha256=9XMFl2HGKtXPzs9ATPRwuQmsuu_Na1hG9R9X_YdAhao,4310
93
+ iris_pex_embedded_python-3.7.2b2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
94
+ iris_pex_embedded_python-3.7.2b2.dist-info/entry_points.txt,sha256=pj-i4LSDyiSP6xpHlVjMCbg1Pik7dC3_sdGY3Yp9Vhk,38
95
+ iris_pex_embedded_python-3.7.2b2.dist-info/top_level.txt,sha256=4p0q6hCATmYIVMVi3I8hOUcJE1kwzyBeHygWv_rGvrU,13
96
+ iris_pex_embedded_python-3.7.2b2.dist-info/RECORD,,