iris-pex-embedded-python 3.7.2b2__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.
Files changed (119) hide show
  1. iop/__init__.py +276 -18
  2. iop/__main__.py +3 -3
  3. iop/cli/__init__.py +1 -0
  4. iop/cli/formatting.py +45 -0
  5. iop/{_cli.py → cli/main.py} +238 -216
  6. iop/cli/parser.py +165 -0
  7. iop/cli/types.py +73 -0
  8. iop/cls/IOP/Common.cls +11 -2
  9. iop/cls/IOP/PrivateSession/Duplex.cls +1 -1
  10. iop/cls/IOP/Service/Remote/Rest/v1.cls +152 -5
  11. iop/cls/IOP/Utils.cls +814 -7
  12. iop/cls/IOP/Wrapper.cls +1 -1
  13. iop/components/__init__.py +16 -0
  14. iop/{_async_request.py → components/async_request.py} +7 -7
  15. iop/components/business_host.py +363 -0
  16. iop/components/business_operation.py +84 -0
  17. iop/{_business_process.py → components/business_process.py} +145 -125
  18. iop/components/business_service.py +106 -0
  19. iop/{_common.py → components/common.py} +160 -164
  20. iop/{_debugpy.py → components/debugpy.py} +11 -11
  21. iop/{_generator_request.py → components/generator_request.py} +4 -4
  22. iop/{_inbound_adapter.py → components/inbound_adapter.py} +10 -18
  23. iop/{_log_manager.py → components/log_manager.py} +1 -1
  24. iop/{_outbound_adapter.py → components/outbound_adapter.py} +1 -1
  25. iop/components/polling_business_service.py +48 -0
  26. iop/{_private_session_duplex.py → components/private_session_duplex.py} +7 -9
  27. iop/{_private_session_process.py → components/private_session_process.py} +2 -2
  28. iop/{_settings.py → components/settings.py} +23 -3
  29. iop/messages/__init__.py +7 -0
  30. iop/messages/base.py +61 -0
  31. iop/{_decorators.py → messages/decorators.py} +8 -2
  32. iop/messages/dispatch.py +351 -0
  33. iop/{_persistent_message.py → messages/persistent.py} +24 -19
  34. iop/{_serialization.py → messages/serialization.py} +34 -21
  35. iop/{_message_validator.py → messages/validation.py} +15 -13
  36. iop/migration/__init__.py +1 -0
  37. iop/migration/io.py +61 -0
  38. iop/migration/manifest.py +534 -0
  39. iop/migration/plans.py +80 -0
  40. iop/migration/utils.py +1126 -0
  41. iop/production/__init__.py +55 -0
  42. iop/production/actions.py +525 -0
  43. iop/production/common.py +103 -0
  44. iop/production/component.py +234 -0
  45. iop/production/declarations.py +190 -0
  46. iop/production/declarative.py +214 -0
  47. iop/production/diff.py +327 -0
  48. iop/production/import_.py +296 -0
  49. iop/production/inspection.py +92 -0
  50. iop/production/model.py +1143 -0
  51. iop/production/planning.py +678 -0
  52. iop/production/reconstruction.py +341 -0
  53. iop/production/rendering.py +610 -0
  54. iop/production/runtime.py +91 -0
  55. iop/production/source_inference.py +729 -0
  56. iop/production/types.py +546 -0
  57. iop/production/validation.py +452 -0
  58. iop/runtime/__init__.py +1 -0
  59. iop/runtime/director.py +415 -0
  60. iop/runtime/environment.py +59 -0
  61. iop/{_iris.py → runtime/iris.py} +2 -2
  62. iop/runtime/local.py +145 -0
  63. iop/{_director_protocol.py → runtime/protocol.py} +25 -8
  64. iop/runtime/remote/__init__.py +12 -0
  65. iop/runtime/remote/client.py +90 -0
  66. iop/runtime/remote/director.py +283 -0
  67. iop/runtime/remote/migration.py +57 -0
  68. iop/runtime/remote/settings.py +69 -0
  69. iop/runtime/remote/setup.py +72 -0
  70. {iris_pex_embedded_python-3.7.2b2.dist-info → iris_pex_embedded_python-4.0.0.dist-info}/METADATA +11 -2
  71. iris_pex_embedded_python-4.0.0.dist-info/RECORD +99 -0
  72. iris_pex_embedded_python-4.0.0.dist-info/entry_points.txt +2 -0
  73. iris_pex_embedded_python-4.0.0.dist-info/top_level.txt +1 -0
  74. grongier/__init__.py +0 -0
  75. grongier/cls/Grongier/PEX/BusinessOperation.cls +0 -8
  76. grongier/cls/Grongier/PEX/BusinessProcess.cls +0 -13
  77. grongier/cls/Grongier/PEX/BusinessService.cls +0 -8
  78. grongier/cls/Grongier/PEX/Common.cls +0 -10
  79. grongier/cls/Grongier/PEX/Director.cls +0 -10
  80. grongier/cls/Grongier/PEX/Duplex/Operation.cls +0 -4
  81. grongier/cls/Grongier/PEX/Duplex/Process.cls +0 -13
  82. grongier/cls/Grongier/PEX/Duplex/Service.cls +0 -4
  83. grongier/cls/Grongier/PEX/InboundAdapter.cls +0 -8
  84. grongier/cls/Grongier/PEX/Message.cls +0 -13
  85. grongier/cls/Grongier/PEX/OutboundAdapter.cls +0 -8
  86. grongier/cls/Grongier/PEX/PickleMessage.cls +0 -13
  87. grongier/cls/Grongier/PEX/PrivateSession/Duplex.cls +0 -8
  88. grongier/cls/Grongier/PEX/PrivateSession/Message/Ack.cls +0 -14
  89. grongier/cls/Grongier/PEX/PrivateSession/Message/Poll.cls +0 -14
  90. grongier/cls/Grongier/PEX/PrivateSession/Message/Start.cls +0 -14
  91. grongier/cls/Grongier/PEX/PrivateSession/Message/Stop.cls +0 -14
  92. grongier/cls/Grongier/PEX/Test.cls +0 -10
  93. grongier/cls/Grongier/PEX/Utils.cls +0 -10
  94. grongier/cls/Grongier/Service/WSGI.cls +0 -4
  95. grongier/pex/__init__.py +0 -75
  96. grongier/pex/__main__.py +0 -4
  97. grongier/pex/_business_host.py +0 -1
  98. grongier/pex/_cli.py +0 -4
  99. grongier/pex/_common.py +0 -1
  100. grongier/pex/_director.py +0 -1
  101. grongier/pex/_utils.py +0 -1
  102. grongier/pex/wsgi/handlers.py +0 -104
  103. iop/_business_host.py +0 -322
  104. iop/_business_operation.py +0 -77
  105. iop/_business_service.py +0 -67
  106. iop/_director.py +0 -333
  107. iop/_dispatch.py +0 -194
  108. iop/_local.py +0 -106
  109. iop/_message.py +0 -45
  110. iop/_polling_business_service.py +0 -6
  111. iop/_remote.py +0 -473
  112. iop/_utils.py +0 -912
  113. iop/cls/IOP/Service/WSGI.cls +0 -310
  114. iop/wsgi/handlers.py +0 -112
  115. iris_pex_embedded_python-3.7.2b2.dist-info/RECORD +0 -96
  116. iris_pex_embedded_python-3.7.2b2.dist-info/entry_points.txt +0 -2
  117. iris_pex_embedded_python-3.7.2b2.dist-info/top_level.txt +0 -2
  118. {iris_pex_embedded_python-3.7.2b2.dist-info → iris_pex_embedded_python-4.0.0.dist-info}/WHEEL +0 -0
  119. {iris_pex_embedded_python-3.7.2b2.dist-info → iris_pex_embedded_python-4.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -4,7 +4,7 @@ import importlib
4
4
  import inspect
5
5
  import os
6
6
  import sys
7
- from typing import Any, Optional
7
+ from typing import Any
8
8
 
9
9
  import iris_persistence
10
10
  from iris_persistence import Field as Field
@@ -12,6 +12,7 @@ from iris_persistence import Model
12
12
  from iris_persistence.models import ModelMeta
13
13
  from iris_persistence.runtime import get_runtime
14
14
 
15
+ from ..runtime.environment import prepend_sys_path
15
16
 
16
17
  DEFAULT_SUPERCLASS = "Ens.MessageBody"
17
18
  DEFAULT_SYNC_MODE = "extend"
@@ -25,7 +26,7 @@ _IRIS_TO_PYTHON_CACHE: dict[str, str] = {}
25
26
  _IRIS_TO_PYTHON_CLASSPATH_CACHE: dict[str, str] = {}
26
27
  _IRIS_TO_PYTHON_STRICT_CACHE: dict[str, bool] = {}
27
28
  _IRIS_TO_MESSAGE_CLASS_CACHE: dict[str, type] = {}
28
- _IRIS_PARAMETER_CACHE: dict[tuple[str, str], Optional[str]] = {}
29
+ _IRIS_PARAMETER_CACHE: dict[tuple[str, str], str | None] = {}
29
30
  _AUTO_SYNCED: set[tuple[type, str]] = set()
30
31
 
31
32
 
@@ -54,18 +55,18 @@ class _PersistentMessageMeta(ModelMeta):
54
55
  cls._iop_persistent_message_abstract = False
55
56
 
56
57
 
57
- def _explicit_meta_classname(cls: type) -> Optional[str]:
58
+ def _explicit_meta_classname(cls: type) -> str | None:
58
59
  meta = cls.__dict__.get("Meta")
59
60
  if meta is None or not hasattr(meta, "classname"):
60
61
  return None
61
- value = getattr(meta, "classname")
62
+ value = meta.classname
62
63
  return str(value) if value else None
63
64
 
64
65
 
65
66
  def _apply_persistent_message_defaults(
66
67
  cls: type,
67
- namespace: Optional[dict] = None,
68
- kwargs: Optional[dict] = None,
68
+ namespace: dict | None = None,
69
+ kwargs: dict | None = None,
69
70
  ) -> None:
70
71
  namespace = namespace or {}
71
72
  kwargs = kwargs or {}
@@ -90,9 +91,17 @@ def _apply_persistent_message_defaults(
90
91
 
91
92
 
92
93
  class _PersistentMessage(Model, metaclass=_PersistentMessageMeta):
94
+ """Base class for native persistent IRIS message bodies.
95
+
96
+ Use PersistentMessage when IRIS needs a generated native message class or
97
+ persistent message body. Prefer Message or PydanticMessage for Python-only
98
+ production routing. See docs/getting-started/register-component.md and
99
+ docs/settings.md.
100
+ """
101
+
93
102
  _iop_persistent_message_abstract = True
94
103
 
95
- def get_iris_id(self) -> Optional[str]:
104
+ def get_iris_id(self) -> str | None:
96
105
  if self.pk:
97
106
  return self.pk
98
107
 
@@ -200,7 +209,7 @@ def serialize_persistent_message(
200
209
 
201
210
  def deserialize_persistent_message(
202
211
  serial: Any,
203
- iris_classname: Optional[str] = None,
212
+ iris_classname: str | None = None,
204
213
  ) -> Any:
205
214
  iris_classname = iris_classname or get_iris_object_classname(serial)
206
215
  if not iris_classname:
@@ -263,7 +272,7 @@ def resolve_iris_classname(msg_cls: type) -> str:
263
272
  return iris_classname
264
273
 
265
274
 
266
- def resolve_python_classname(iris_classname: str) -> Optional[str]:
275
+ def resolve_python_classname(iris_classname: str) -> str | None:
267
276
  python_classname, _python_classpath, _strict = _resolve_python_message_metadata(
268
277
  iris_classname
269
278
  )
@@ -271,7 +280,7 @@ def resolve_python_classname(iris_classname: str) -> Optional[str]:
271
280
 
272
281
 
273
282
  def load_python_class(
274
- python_classname: str, python_classpath: Optional[str] = None
283
+ python_classname: str, python_classpath: str | None = None
275
284
  ) -> type:
276
285
  module_name, _, class_name = python_classname.rpartition(".")
277
286
  if not module_name:
@@ -306,7 +315,7 @@ def get_python_classpath(klass: type) -> str:
306
315
  return classpath
307
316
 
308
317
 
309
- def get_iris_object_classname(obj: Any) -> Optional[str]:
318
+ def get_iris_object_classname(obj: Any) -> str | None:
310
319
  for method_name in ("%ClassName", "_ClassName"):
311
320
  try:
312
321
  method = getattr(obj, method_name)
@@ -322,7 +331,7 @@ def get_iris_object_classname(obj: Any) -> Optional[str]:
322
331
  return None
323
332
 
324
333
 
325
- def _safe_get_object_id(obj: Any) -> Optional[str]:
334
+ def _safe_get_object_id(obj: Any) -> str | None:
326
335
  for method_name in ("_Id", "Id", "%Id"):
327
336
  try:
328
337
  method = getattr(obj, method_name)
@@ -387,7 +396,7 @@ def _cache_mapping(python_classname: str, iris_classname: str) -> None:
387
396
 
388
397
  def _resolve_python_message_metadata(
389
398
  iris_classname: str,
390
- ) -> tuple[Optional[str], Optional[str], bool]:
399
+ ) -> tuple[str | None, str | None, bool]:
391
400
  cached = _IRIS_TO_PYTHON_CACHE.get(iris_classname)
392
401
  if cached:
393
402
  return (
@@ -417,7 +426,7 @@ def _resolve_python_message_metadata(
417
426
  def get_iris_class_parameter(
418
427
  iris_classname: str,
419
428
  parameter_name: str,
420
- ) -> Optional[str]:
429
+ ) -> str | None:
421
430
  key = (iris_classname, parameter_name)
422
431
  if key in _IRIS_PARAMETER_CACHE:
423
432
  return _IRIS_PARAMETER_CACHE[key]
@@ -511,11 +520,7 @@ def _decode_iris_identifier(value: str) -> str:
511
520
 
512
521
 
513
522
  def _prepend_sys_path(path: str) -> None:
514
- normalized_path = os.path.abspath(os.path.normpath(path))
515
- while normalized_path in sys.path:
516
- sys.path.remove(normalized_path)
517
- sys.path.insert(0, normalized_path)
518
- importlib.invalidate_caches()
523
+ prepend_sys_path(path)
519
524
 
520
525
 
521
526
  def _import_module_from_classpath(module_name: str, python_classpath: str):
@@ -1,17 +1,19 @@
1
1
  from __future__ import annotations
2
+
2
3
  import codecs
4
+ import copy
3
5
  import importlib
4
6
  import inspect
5
- import pickle
6
7
  import json
8
+ import pickle
7
9
  from dataclasses import is_dataclass
8
- from typing import Any, Dict, Type
10
+ from typing import Any
9
11
 
10
12
  from pydantic import BaseModel, TypeAdapter, ValidationError
11
13
 
12
- from . import _iris
13
- from ._message import _Message
14
- from ._utils import _Utils
14
+ from ..migration import utils as _migration_utils
15
+ from ..runtime import iris as _iris
16
+ from .base import _Message
15
17
 
16
18
 
17
19
  class SerializationError(Exception):
@@ -20,6 +22,12 @@ class SerializationError(Exception):
20
22
  pass
21
23
 
22
24
 
25
+ class MessageClassImportError(SerializationError, ImportError):
26
+ """Raised when a JSON message's Python class cannot be imported."""
27
+
28
+ pass
29
+
30
+
23
31
  class TempPydanticModel(BaseModel):
24
32
  model_config = {"arbitrary_types_allowed": True, "extra": "allow"}
25
33
 
@@ -32,7 +40,6 @@ class MessageSerializer:
32
40
  message: Any, use_pickle: bool = False, is_generator: bool = False
33
41
  ) -> Any:
34
42
  """Serializes a message to IRIS format."""
35
- message = remove_iris_id(message)
36
43
  if use_pickle:
37
44
  return MessageSerializer._serialize_pickle(message, is_generator)
38
45
  return MessageSerializer._serialize_json(message, is_generator)
@@ -48,7 +55,7 @@ class MessageSerializer:
48
55
  msg.classname = f"{message.__class__.__module__}.{message.__class__.__name__}"
49
56
 
50
57
  if hasattr(msg, "buffer") and len(json_string) > msg.buffer:
51
- msg.json = _Utils.string_to_stream(json_string, msg.buffer)
58
+ msg.json = _migration_utils.string_to_stream(json_string, msg.buffer)
52
59
  else:
53
60
  msg.json = json_string
54
61
  return msg
@@ -63,7 +70,7 @@ class MessageSerializer:
63
70
  else:
64
71
  msg = _iris.get_iris().cls("IOP.PickleMessage")._New()
65
72
  msg.classname = f"{message.__class__.__module__}.{message.__class__.__name__}"
66
- msg.jstr = _Utils.string_to_stream(pickle_string)
73
+ msg.jstr = _migration_utils.string_to_stream(pickle_string)
67
74
  return msg
68
75
 
69
76
  @staticmethod
@@ -92,10 +99,10 @@ class MessageSerializer:
92
99
  )
93
100
  module = importlib.import_module(module_name)
94
101
  msg_class = getattr(module, class_name)
95
- except Exception as e:
96
- raise SerializationError(
102
+ except (ModuleNotFoundError, AttributeError, ValueError) as e:
103
+ raise MessageClassImportError(
97
104
  f"Failed to load class {serial.classname}: {str(e)}"
98
- )
105
+ ) from e
99
106
 
100
107
  if is_dataclass(msg_class) and issubclass(msg_class, BaseModel):
101
108
  raise SerializationError(
@@ -105,7 +112,7 @@ class MessageSerializer:
105
112
  )
106
113
 
107
114
  json_string = (
108
- _Utils.stream_to_string(serial.json)
115
+ _migration_utils.stream_to_string(serial.json)
109
116
  if serial.type == "Stream"
110
117
  else serial.json
111
118
  )
@@ -120,18 +127,21 @@ class MessageSerializer:
120
127
  f"Class {msg_class} must be a Pydantic model or dataclass"
121
128
  )
122
129
  except Exception as e:
123
- raise SerializationError(f"Failed to deserialize JSON: {str(e)}")
130
+ raise SerializationError(f"Failed to deserialize JSON: {str(e)}") from e
124
131
 
125
132
  @staticmethod
126
133
  def _deserialize_pickle(serial: Any) -> Any:
127
- string = _Utils.stream_to_string(serial.jstr)
134
+ string = _migration_utils.stream_to_string(serial.jstr)
128
135
  return pickle.loads(codecs.decode(string.encode(), "base64"))
129
136
 
130
137
  @staticmethod
131
138
  def _parse_classname(classname: str) -> tuple[str, str]:
132
- j = classname.rindex(".")
139
+ try:
140
+ j = classname.rindex(".")
141
+ except ValueError as e:
142
+ raise ValueError(f"Classname must include a module: {classname}") from e
133
143
  if j <= 0:
134
- raise SerializationError(f"Classname must include a module: {classname}")
144
+ raise ValueError(f"Classname must include a module: {classname}")
135
145
  return classname[:j], classname[j + 1 :]
136
146
 
137
147
  @staticmethod
@@ -156,18 +166,19 @@ class MessageSerializer:
156
166
 
157
167
 
158
168
  def remove_iris_id(message: Any) -> Any:
169
+ message = copy.copy(message)
159
170
  try:
160
- del message._iris_id
171
+ delattr(message, "_iris_id")
161
172
  except AttributeError:
162
173
  pass
163
174
  return message
164
175
 
165
176
 
166
- def dataclass_from_dict(klass: Type | Any, dikt: Dict) -> Any:
177
+ def dataclass_from_dict(klass: type | Any, dikt: dict) -> Any:
167
178
  """Converts a dictionary to a dataclass instance.
168
179
  Handles non attended fields and nested dataclasses."""
169
180
 
170
- def process_field(value: Any, field_type: Type) -> Any:
181
+ def process_field(value: Any, field_type: type) -> Any:
171
182
  if value is None:
172
183
  return None
173
184
  if is_dataclass(field_type):
@@ -203,11 +214,13 @@ def dataclass_from_dict(klass: Type | Any, dikt: Dict) -> Any:
203
214
  return instance
204
215
 
205
216
 
206
- def dataclass_to_dict(instance: Any) -> Dict:
217
+ def dataclass_to_dict(instance: Any) -> dict:
207
218
  """Converts a class instance to a dictionary.
208
219
  Handles non attended fields."""
209
220
  result = {}
210
221
  for field in instance.__dict__:
222
+ if field == "_iris_id":
223
+ continue
211
224
  value = getattr(instance, field)
212
225
  if is_dataclass(value):
213
226
  result[field] = dataclass_to_dict(value)
@@ -227,7 +240,7 @@ def dataclass_to_dict(instance: Any) -> Dict:
227
240
  return result
228
241
 
229
242
 
230
- # Maintain backwards compatibility
243
+ # Convenience wrappers used by dispatch and tests.
231
244
  def serialize_pickle_message(msg):
232
245
  return MessageSerializer.serialize(msg, use_pickle=True, is_generator=False)
233
246
 
@@ -1,12 +1,14 @@
1
1
  import dataclasses
2
- from typing import Any, Type
2
+ from typing import Any
3
3
 
4
- from ._message import _Message, _PickleMessage, _PydanticPickleMessage, BaseModel
4
+ from .base import _Message, _PickleMessage, _PydanticMessage, _PydanticPickleMessage
5
+
6
+ _PYDANTIC_BASE = _PydanticMessage.__mro__[1]
5
7
 
6
8
 
7
9
  def is_message_instance(obj: Any) -> bool:
8
10
  """Check if object is a valid Message instance."""
9
- if isinstance(obj, BaseModel):
11
+ if isinstance(obj, _PYDANTIC_BASE):
10
12
  return True
11
13
  if is_message_class(type(obj)):
12
14
  if not dataclasses.is_dataclass(obj):
@@ -36,17 +38,17 @@ def is_iris_object_instance(obj: Any) -> bool:
36
38
  # Stream.Object are used for HTTP InboundAdapter/OutboundAdapter
37
39
 
38
40
 
39
- def is_message_class(klass: Type) -> bool:
41
+ def is_message_class(klass: type) -> bool:
40
42
  """Check if class is a Message type."""
41
- if issubclass(klass, _Message):
42
- return True
43
- return False
43
+ try:
44
+ return issubclass(klass, (_Message, _PYDANTIC_BASE))
45
+ except TypeError:
46
+ return False
44
47
 
45
48
 
46
- def is_pickle_message_class(klass: Type) -> bool:
49
+ def is_pickle_message_class(klass: type) -> bool:
47
50
  """Check if class is a PickleMessage type."""
48
- if issubclass(klass, _PickleMessage):
49
- return True
50
- if issubclass(klass, _PydanticPickleMessage):
51
- return True
52
- return False
51
+ try:
52
+ return issubclass(klass, (_PickleMessage, _PydanticPickleMessage))
53
+ except TypeError:
54
+ return False
@@ -0,0 +1 @@
1
+ from .utils import _Utils as _Utils
iop/migration/io.py ADDED
@@ -0,0 +1,61 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from typing import Any
6
+
7
+ import xmltodict
8
+
9
+
10
+ def dict_to_xml(data: dict[str, Any]) -> str:
11
+ xml = xmltodict.unparse(data, pretty=True)
12
+ xml = xml.replace('<?xml version="1.0" encoding="utf-8"?>', "")
13
+ return xml[1:]
14
+
15
+
16
+ def xml_to_json(xml_string: str) -> str:
17
+ """Convert an XML production export to the JSON shape used by Production."""
18
+
19
+ def postprocessor(path, key, value):
20
+ return key, "" if value is None else value
21
+
22
+ data = xmltodict.parse(xml_string, postprocessor=postprocessor)
23
+ if "Production" in data:
24
+ production_obj = data["Production"]
25
+ production_name = production_obj.get("@Name", "Production")
26
+ data = {production_name: production_obj}
27
+ return json.dumps(data)
28
+
29
+
30
+ def stream_to_string(stream, buffer=1000000) -> str:
31
+ string = ""
32
+ stream.Rewind()
33
+ while not stream.AtEnd:
34
+ string += stream.Read(buffer)
35
+ return string
36
+
37
+
38
+ def string_to_stream(iris, string: str, buffer=1000000):
39
+ stream = iris.cls("%Stream.GlobalCharacter")._New()
40
+ chunks = [string[i : i + buffer] for i in range(0, len(string), buffer)]
41
+ for chunk in chunks:
42
+ stream.Write(chunk)
43
+ return stream
44
+
45
+
46
+ def guess_path(module: str, path: str) -> str:
47
+ """Determine the full file path for a module name under a base path."""
48
+ if not module:
49
+ raise ValueError("Module name cannot be empty")
50
+
51
+ if module.startswith("."):
52
+ dot_count = len(module) - len(module.lstrip("."))
53
+ module = module[dot_count:]
54
+ for _ in range(dot_count - 1):
55
+ path = os.path.dirname(path)
56
+
57
+ if module.endswith(".py"):
58
+ module_path = module.replace(".", os.sep)
59
+ else:
60
+ module_path = module.replace(".", os.sep) + ".py"
61
+ return os.path.join(path, module_path)