mi-mdb 0.1.0__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.
mi_mdb-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Leon Starr
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ include src/mdb/log.conf
mi_mdb-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,49 @@
1
+ Metadata-Version: 2.4
2
+ Name: mi-mdb
3
+ Version: 0.1.0
4
+ Summary: Shlaer-Mellor Executable UML Model Debugger
5
+ Author-email: Leon Starr <leon_starr@modelint.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Leon Starr
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: repository, https://github.com/modelint/model-debugger
29
+ Project-URL: documentation, https://github.com/modelint/model-debugger/wiki
30
+ Keywords: shlaer-mellor,metamodel,executable uml,mbse,xuml,xtuml,model debugger,platform independent
31
+ Classifier: License :: OSI Approved :: MIT License
32
+ Classifier: Programming Language :: Python
33
+ Classifier: Programming Language :: Python :: 3
34
+ Requires-Python: >=3.11
35
+ Description-Content-Type: text/markdown
36
+ License-File: LICENSE
37
+ Requires-Dist: mi-mx
38
+ Requires-Dist: mi-pyral
39
+ Requires-Dist: PyYAML
40
+ Provides-Extra: build
41
+ Requires-Dist: build; extra == "build"
42
+ Requires-Dist: twine; extra == "build"
43
+ Provides-Extra: dev
44
+ Requires-Dist: bump2version; extra == "dev"
45
+ Requires-Dist: pytest; extra == "dev"
46
+ Dynamic: license-file
47
+
48
+ # model-debugger
49
+ A modeling language independent model debugger
mi_mdb-0.1.0/README.md ADDED
@@ -0,0 +1,2 @@
1
+ # model-debugger
2
+ A modeling language independent model debugger
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "mi-mdb"
7
+ version = "0.1.0"
8
+ description = "Shlaer-Mellor Executable UML Model Debugger"
9
+ readme = "README.md"
10
+ authors = [{ name = "Leon Starr", email = "leon_starr@modelint.com" }]
11
+ license = { file = "LICENSE" }
12
+ classifiers = [
13
+ "License :: OSI Approved :: MIT License",
14
+ "Programming Language :: Python",
15
+ "Programming Language :: Python :: 3",
16
+ ]
17
+ keywords = ["shlaer-mellor", "metamodel", "executable uml", "mbse", "xuml", "xtuml", "model debugger", "platform independent"]
18
+ dependencies = ["mi-mx", "mi-pyral", "PyYAML"]
19
+ requires-python = ">=3.11"
20
+
21
+ [project.optional-dependencies]
22
+ build = ["build", "twine"]
23
+ dev = ["bump2version", "pytest"]
24
+
25
+ [project.scripts]
26
+ mdb = "mdb.__main__:main"
27
+
28
+ [project.urls]
29
+ repository = "https://github.com/modelint/model-debugger"
30
+ documentation = "https://github.com/modelint/model-debugger/wiki"
mi_mdb-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ version = "0.1.0"
@@ -0,0 +1,85 @@
1
+ """
2
+ Blueprint Model Debugger
3
+
4
+ """
5
+ # System
6
+ import logging
7
+ import logging.config
8
+ import sys
9
+ import argparse
10
+ from pathlib import Path
11
+ import atexit
12
+
13
+ # MDB
14
+ from mdb.session import Session
15
+ from mdb import version
16
+
17
+ _logpath = Path("mdb.log")
18
+ _progname = 'Blueprint Model Debugger'
19
+
20
+ def clean_up():
21
+ """Normal and exception exit activities"""
22
+ _logpath.unlink(missing_ok=True)
23
+
24
+ def get_logger():
25
+ """Initiate the logger"""
26
+ log_conf_path = Path(__file__).parent / 'log.conf' # Logging configuration is in this file
27
+ logging.config.fileConfig(fname=log_conf_path, disable_existing_loggers=False)
28
+ return logging.getLogger(__name__) # Create a logger for this module
29
+
30
+
31
+ # Configure the expected parameters and actions for the argparse module
32
+ def parse(cl_input):
33
+ parser = argparse.ArgumentParser(description=_progname)
34
+ parser.add_argument('-s', '--system', action='store',
35
+ help='Name of the metamodel TclRAL database *.ral file populated with one or more domains')
36
+ parser.add_argument('-p', '--playground', action='store',
37
+ help='Name of the context directory specifying the initialized domain dbs and a *.sip file')
38
+ parser.add_argument('-x', '--scenario', action='store',
39
+ help='Name of the scenario *.yaml file to run against the populated system')
40
+ parser.add_argument('-L', '--log', action='store_true',
41
+ help='Generate a diagnostic log file')
42
+ parser.add_argument('-v', '--verbose', action='store_true',
43
+ help='Verbose messages')
44
+ parser.add_argument('-V', '--version', action='store_true',
45
+ help='Print the current version of the model execution app')
46
+ return parser.parse_args(cl_input)
47
+
48
+
49
+ def main():
50
+ # Start logging
51
+ logger = get_logger()
52
+ msg = f'{_progname} version: {version}\n'
53
+ logger.info(msg)
54
+
55
+ # Parse the command line args
56
+ args = parse(sys.argv[1:])
57
+
58
+ if args.version:
59
+ # Just print the version and quit
60
+ print(f'{_progname} version: {version}')
61
+ sys.exit(0)
62
+
63
+ if not args.log:
64
+ # If no log file is requested, remove the log file before termination
65
+ atexit.register(clean_up)
66
+
67
+ print(f"\n{msg}")
68
+ if args.verbose:
69
+ print("\nVerbose mode set\n")
70
+
71
+ # All pathnames are optional since the user can specify them in the debug session
72
+ session = Session() # Create the singleton instance
73
+ session.initialize(
74
+ system_path=Path(args.system) if args.system else None,
75
+ playground_name=args.playground if args.playground else None,
76
+ scenario_name=args.scenario if args.scenario else None,
77
+ verbose=args.verbose)
78
+
79
+ logger.info("No problemo") # We didn't die on an exception, basically
80
+ if args.verbose:
81
+ print("\nNo problemo")
82
+
83
+
84
+ if __name__ == "__main__":
85
+ main()
@@ -0,0 +1,136 @@
1
+ """ system.py -- System """
2
+
3
+ # System
4
+ from pathlib import Path
5
+ from typing import Optional
6
+ from enum import Enum
7
+
8
+ from mdb.ui_types import *
9
+
10
+ class SystemState(Enum):
11
+ LOADED = "LOADED"
12
+ NOT_LOADED = "NOT_LOADED"
13
+ RUNNING = "RUNNING"
14
+ SUSPENDED = "SUSPENDED"
15
+ TERMINATED = "TERMINATED"
16
+
17
+ def validate_path(path: Path) -> bool:
18
+ """
19
+ Validates that a Path exists and is accessible.
20
+ Returns True if valid, prints error to console if not and returns False
21
+ """
22
+ if not path.is_absolute():
23
+ print(f"Path is not absolute: {path}")
24
+ return False
25
+
26
+ if not path.exists():
27
+ print(f"Path does not exist: {path}")
28
+ return False
29
+
30
+ if not path.is_file() and not path.is_dir():
31
+ print(f"Path is neither a file nor a directory: {path}")
32
+ return False
33
+
34
+ return True
35
+
36
+
37
+ class System:
38
+ """
39
+ The System being tested
40
+
41
+ """
42
+
43
+ def __init__(self, system_path: Optional[Path] = None, context_dir: Optional[Path] = None):
44
+
45
+ self.path = system_path
46
+ self.name = system_path.name if system_path else None
47
+ self.mmdb_fname = None
48
+ self.playground = None
49
+ self.state = SystemState.NOT_LOADED
50
+
51
+ if self.path:
52
+ self.load_models()
53
+
54
+ def load_models(self):
55
+ """
56
+ Tell the MX to load the system
57
+
58
+ Returns:
59
+
60
+ """
61
+ print(f"Loading models for: {self.name}{STATUS}")
62
+ self.state = SystemState.LOADED
63
+ model_path = self.path / 'models'
64
+ ral_files = list(model_path.glob("*.ral"))
65
+ #
66
+ if len(ral_files) == 0:
67
+ print(f"Error: No .ral file found in '{self.path}'")
68
+ return None
69
+ elif len(ral_files) > 1:
70
+ print(f"Error: Multiple .ral files found in '{self.path}': {[f.name for f in ral_files]}")
71
+ return None
72
+
73
+ self.mmdb_fname = Path(ral_files[0])
74
+
75
+ print(f"Model file is: {self.mmdb_fname.name}")
76
+
77
+ def set_playground(self, playground_name: str):
78
+ """
79
+ Validate path and, if valid, set it
80
+
81
+ Args:
82
+ playground_name:
83
+ """
84
+ if validate_path(self.path / 'playgrounds' / playground_name ):
85
+ self.playground = playground_name
86
+
87
+ def set_path(self, system_path: Path):
88
+ """
89
+ Validate path and, if valid, set it
90
+
91
+ Args:
92
+ system_path:
93
+ """
94
+ # TODO: Ensure that System is in a compatible state such as NOT_LOADED
95
+ if validate_path(system_path):
96
+ self.path = system_path
97
+ # Reset local sub-paths
98
+ # TODO: changing the path should result in a new instance of System
99
+ self.playground = None
100
+ self.mmdb_fname = None
101
+
102
+ def show_playgrounds(self):
103
+ play_path = self.path / 'playgrounds'
104
+ pg_dirs = [f for f in play_path.iterdir() if f.is_dir()]
105
+ for d in pg_dirs:
106
+ print(f" {d.name}")
107
+
108
+ def show_active_playground(self):
109
+ if self.playground:
110
+ print(f" Active playground: {self.playground}")
111
+ return
112
+ print(" No active playground")
113
+
114
+ def show_path(self):
115
+ if self.path:
116
+ print(f"System path is: [{self.path}]")
117
+ else:
118
+ print(f"System path not set")
119
+
120
+ def show_models(self, classes: list[str]):
121
+ """
122
+ Print out the loaded models
123
+
124
+ Args:
125
+ classes: A list of requested metamodel classes to view. If list is empty, entire mmdb is displayed
126
+ """
127
+ pass
128
+
129
+ def load_context(self):
130
+ """
131
+ Tell the MX to load the context
132
+
133
+ Returns:
134
+
135
+ """
136
+ pass
@@ -0,0 +1,48 @@
1
+ [loggers]
2
+ keys=root,MDBLogger,PyralLogger
3
+
4
+ [handlers]
5
+ keys=fileHandler, consoleHandler, consoleHandlerUser
6
+
7
+ [formatters]
8
+ keys=MDBFormatter, MDBFormatterUser
9
+
10
+ [logger_root]
11
+ level=DEBUG
12
+ handlers=fileHandler, consoleHandlerUser
13
+
14
+ [logger_PyralLogger]
15
+ level=WARNING
16
+ handlers=fileHandler
17
+ qualname=pyral
18
+ propagate=0
19
+
20
+ [logger_MDBLogger]
21
+ level=DEBUG
22
+ handlers=fileHandler, consoleHandlerUser
23
+ qualname=MDBLogger
24
+ propagate=0
25
+
26
+ [handler_fileHandler]
27
+ class=FileHandler
28
+ level=DEBUG
29
+ formatter=MDBFormatter
30
+ args=('mdb.log', 'w')
31
+
32
+ [handler_consoleHandlerUser]
33
+ class=StreamHandler
34
+ level=WARNING
35
+ formatter=MDBFormatterUser
36
+ args=(sys.stderr,)
37
+
38
+ [handler_consoleHandler]
39
+ class=StreamHandler
40
+ level=WARNING
41
+ formatter=MDBFormatter
42
+ args=(sys.stderr,)
43
+
44
+ [formatter_MDBFormatter]
45
+ format= MDB: %(name)s - %(levelname)s - %(message)s
46
+
47
+ [formatter_MDBFormatterUser]
48
+ format=MDB: %(levelname)s - %(message)s
@@ -0,0 +1,48 @@
1
+ """scenario.py -- Scenario class"""
2
+
3
+ # System
4
+ from pathlib import Path
5
+ import yaml
6
+
7
+ # Model Integration
8
+ from mx.mxtypes import *
9
+
10
+ class Scenario:
11
+
12
+ def __init__(self, sfile: Path):
13
+ """
14
+ Args:
15
+ file: Path to a scenario yaml file
16
+ """
17
+ # Load the yaml file
18
+ with open(sfile, "r") as file:
19
+ sdata = yaml.safe_load(file) # Load YAML content safely
20
+
21
+ # Unpack the Actors
22
+ actors = {}
23
+ internal_actor_parse = sdata['Actors']['internal']
24
+ for domain, instances in internal_actor_parse.items():
25
+ for name, address in instances.items():
26
+ instance_id = {attr: value for attr, value in address['instance'].items()}
27
+ actors[f"{domain}:{name}"] = InstanceAddress(domain=domain, class_name=address['class'],
28
+ instance_id=instance_id)
29
+
30
+ external_actor_parse = sdata['Actors']['external']
31
+ for ea in external_actor_parse:
32
+ actors[ea] = ExternalAddress(domain=ea)
33
+ pass
34
+
35
+ # Unpack the interactions
36
+ self.interactions = []
37
+ for i in sdata['Interactions']:
38
+ ituple = Interaction(
39
+ description=i['description'],
40
+ direction=Direction(i['direction']),
41
+ action=ActionType(i['action']),
42
+ name=i['name'],
43
+ source=actors[i['source']],
44
+ target=actors[i['target']],
45
+ parameters=i.get('parameters', {})
46
+ )
47
+ self.interactions.append(ituple)
48
+ pass
@@ -0,0 +1,423 @@
1
+ """ session.py -- Model Debugger Session """
2
+
3
+ # System
4
+ import shlex
5
+ import sys
6
+ from pathlib import Path
7
+ import logging
8
+ import re
9
+ import yaml
10
+
11
+ # Model Integration
12
+ from mx.system import System
13
+ from mx.mxtypes import *
14
+
15
+ # MDB
16
+ from mdb.ui_types import *
17
+ from mdb.scenario import Scenario
18
+
19
+ STEP_PROMPT = '>: '
20
+
21
+ _logger = logging.getLogger(__name__)
22
+
23
+
24
+ def shortcut_index(s: str) -> int | None:
25
+ """
26
+ Convert value to an integer shortcut if it is a 1-2 char integer with no leading zeros
27
+
28
+ Args:
29
+ s - user string input
30
+ Returns:
31
+ An integer shortcut if detected, otherwise None
32
+ """
33
+ return int(s) if re.fullmatch(r'[1-9]\d?', s) else None
34
+
35
+
36
+ class Session:
37
+ """
38
+ This class represents the debugger session. It follows the singleton pattern to ensure only one exists.
39
+
40
+ Attributes:
41
+ system: MX system object we are debugging
42
+ playground_name: Name of the active playground (same as file name in system playgrounds dir)
43
+ scenario_name: Name of the active scenario (same as file name minus any extension in the
44
+ playground scenarios dir)
45
+ playground_scenarios: All scenario files defined for the active playground minus any file extension
46
+ active_scenario: Name of the scenario we are currently running
47
+ verbose: Whether verbose output is enabled
48
+ stepping: Whether stepping mode is enabled (pauses between interactions)
49
+ descriptions: Whether interaction descriptions are printed during scenario execution
50
+ """
51
+ _instance = None
52
+
53
+ def __new__(cls):
54
+ if cls._instance is None:
55
+ cls._instance = super(Session, cls).__new__(cls)
56
+ return cls._instance
57
+
58
+ def __init__(self):
59
+ # Avoid singleton reinitialization if already initialized
60
+ if getattr(self, "_initialized", False):
61
+ return
62
+ self._initialized = True
63
+
64
+ self.system = None
65
+ self.playground_name = None
66
+ self.scenario_name = None
67
+ self.playground_scenarios: list[str] = []
68
+ self.active_scenario = None
69
+ self.verbose = False
70
+ self.stepping = False # By default we assume that the user wants to run the scenario without stepping
71
+ self.descriptions = False
72
+
73
+ def initialize(self, verbose: bool,
74
+ system_path: Path = None,
75
+ playground_name: str = None,
76
+ scenario_name: str = None,
77
+ ):
78
+ """
79
+ We can initialize a session without any system and then let the user initialize it interactively
80
+ or, if a system path is provided, we can initialize it right away. The same goes for the playground and
81
+ scenario names.
82
+
83
+ After whatever degree of initialization is completed, we start the user command input loop
84
+
85
+ Args:
86
+ verbose: Verbose mode for stdout (console) messaging/status
87
+ system_path: An absolute path to the system directory where the entire system is defined
88
+ playground_name: Name of a playground corresponding ot the name of a file in the system's playground dir
89
+ scenario_name: Name of a scenario file (yaml, for now) that the mdb loads locally, but is found in the
90
+ system's scenarios directory
91
+ """
92
+ print("Initializing session from command line arguments...")
93
+ if system_path:
94
+ self.system = System() # Singleton initialization
95
+ if system_path:
96
+ self.system.initialize(system_path=system_path, verbose=verbose)
97
+ else:
98
+ print("System not yet initialized. Use: set system <path> to initialize.")
99
+ self.scenario_name = scenario_name
100
+ self.playground_name = playground_name
101
+ self.verbose = verbose
102
+
103
+ # Initialize these if they were supplied on the command line
104
+ if playground_name:
105
+ self.set_playground(playground_name)
106
+ if scenario_name:
107
+ self.set_scenario(scenario_name)
108
+
109
+ self.run()
110
+
111
+ def run(self):
112
+ """
113
+ This is the user input command loop. It just runs until the user exits.
114
+ """
115
+
116
+ print("\nType 'help' for available commands, 'quit' or 'exit' to exit\n")
117
+
118
+ while True: # Breaks out when the user enters the quit/exit command
119
+ try:
120
+ raw = input(CMD_PROMPT).strip()
121
+ except (EOFError, KeyboardInterrupt):
122
+ print()
123
+ break
124
+
125
+ if not raw:
126
+ continue
127
+
128
+ try:
129
+ parts = shlex.split(raw)
130
+ except ValueError as e:
131
+ print(f"Parse error: {e}")
132
+ continue
133
+
134
+ command, *args = parts
135
+
136
+ match command.lower():
137
+ case "quit" | "exit":
138
+ break
139
+ case "help":
140
+ self.cmd_help(args)
141
+ case "show":
142
+ self.cmd_show(args)
143
+ case "list":
144
+ self.cmd_list(args)
145
+ case "set":
146
+ self.cmd_set(args)
147
+ case "execute" | "x" | "exec":
148
+ self.execute_scenario()
149
+ case _:
150
+ print(f"Unknown command: '{command}'. Type 'help' for available commands.")
151
+
152
+ def show_stepping_status(self):
153
+ if self.stepping:
154
+ print("Stepping mode set")
155
+ else:
156
+ print("Run to completion mode set")
157
+
158
+ def show_descriptions_status(self):
159
+ if self.descriptions:
160
+ print("Descriptions mode set")
161
+ else:
162
+ print("Descriptions mode not set")
163
+
164
+ def show_states(self) -> None:
165
+ if not self.system:
166
+ print("No system loaded.")
167
+ return
168
+ for domain_name, domain in self.system.domains.items():
169
+ current_states = domain.get_current_states()
170
+ if not current_states:
171
+ continue
172
+ print(f"\n{domain_name} current states:")
173
+ print("—")
174
+ for sm in current_states:
175
+ if sm.instance:
176
+ inst_str = '<' + '-'.join(f"{k}:{v}" for k, v in sm.instance.items()) + '>'
177
+ else:
178
+ inst_str = ""
179
+ print(f"{sm.state_model} {inst_str} [{sm.state}]")
180
+ print("—")
181
+
182
+ def cmd_show(self, args: list[str]) -> None:
183
+ """
184
+ Display requested item on console
185
+
186
+ Args:
187
+ args: What to display
188
+ """
189
+ if len(args) < 1:
190
+ print("Usage: set <item>")
191
+ return
192
+
193
+ item = args[0]
194
+ match item:
195
+ case 'scenario':
196
+ print("Not implemented yet.")
197
+ case 'path':
198
+ self.system.show_path()
199
+ case 'playground' | 'pg':
200
+ print(f"Active playground: {self.system.playground}")
201
+ case 'playgrounds' | 'pgs':
202
+ self.show_playgrounds()
203
+ case 'active':
204
+ if args[1] == 'playground':
205
+ self.system.show_active_playground()
206
+ else:
207
+ print(f"Unknown active element: {args[1]}")
208
+ case 'scenarios':
209
+ self.show_scenarios()
210
+ case 'step':
211
+ self.show_stepping_status()
212
+ case 'descriptions' | 'desc':
213
+ self.show_descriptions_status()
214
+ case 'states':
215
+ self.show_states()
216
+ case _:
217
+ print(f"Unknown item: {item}")
218
+ return
219
+
220
+ pass
221
+ # TODO: Have MX load the ral file and initialize the system
222
+
223
+ def cmd_help(self, args: list[str]) -> None:
224
+ print("Commands:")
225
+ print(" show <item> - Display item on console (ex: show path)")
226
+ print(" show states - Display current state of all state models")
227
+ print(" set <variable> <value> - Set a variable (ex: set path ~/my/path)")
228
+ print(" set step - Toggle stepping mode (pause between interactions)")
229
+ print(" set desc - Toggle descriptions mode (print interaction descriptions)")
230
+ print(" execute / exec / x - Execute the active scenario")
231
+ print(" quit / exit - Exit the debugger")
232
+
233
+ def cmd_set(self, args: list[str]) -> None:
234
+ """
235
+ Set the value of some variable
236
+
237
+ Args:
238
+ args:
239
+ """
240
+ if len(args) > 2:
241
+ print("Usage: set <variable> [<value>]")
242
+ return
243
+
244
+ variable, value = args[0], args[1] if len(args) > 1 else None
245
+ vset = False
246
+ match variable:
247
+ case 'path':
248
+ self.system.set_path(system_path=Path(value))
249
+ case 'playground' | 'pg':
250
+ self.set_playground(value)
251
+ case 'scenario':
252
+ self.set_scenario(value)
253
+ case 'step':
254
+ # Toggle the setting
255
+ self.stepping = not self.stepping
256
+ self.show_stepping_status()
257
+ case 'descriptions' | 'desc':
258
+ # Toggle the setting
259
+ self.descriptions = not self.descriptions
260
+ self.show_descriptions_status()
261
+ case _:
262
+ vset = False
263
+ print(f"Setting {variable} not defined")
264
+
265
+ if vset:
266
+ print(f"{variable} = {value}")
267
+
268
+ def cmd_list(self, args: list[str]) -> None:
269
+ if not args:
270
+ print("Usage: list <target>")
271
+ return
272
+ target = args[0]
273
+ print(f" [stub] list {target}")
274
+
275
+ def show_playgrounds(self):
276
+ """Display all playgrounds defined in the system directory"""
277
+ system_playgrounds = self.system.playgrounds
278
+ if system_playgrounds is not None:
279
+ print("Available playgrounds:")
280
+ for n, p in enumerate(self.system.available_playgrounds):
281
+ print(f" [{n + 1}] - {p}")
282
+
283
+ def set_playground(self, value: str) -> None:
284
+ """
285
+ Verify that the supplied value corresponds to a valid playground.
286
+
287
+ Args:
288
+ value: A full string name of a playground directory or shortcut 1-2 integer index
289
+ """
290
+ i = shortcut_index(value) # i is between 1 and 99 or None
291
+ if i is not None:
292
+ if i > len(self.system.available_playgrounds):
293
+ print(f"Undefined playground shortcut: [{value[1:]}]")
294
+ return
295
+ pg_name = self.system.playgrounds[i - 1] # User counts items starting from 1
296
+ else:
297
+ pg_name = value
298
+ if pg_name not in self.system.playgrounds:
299
+ print(f"Unknown playground: {pg_name}")
300
+ return
301
+ print(f"Selected playground: {pg_name}")
302
+
303
+ self.system.load_domains(playground=pg_name)
304
+ # And find all available scenarios for that playground
305
+ self.show_scenarios()
306
+
307
+ def set_scenario(self, value: str):
308
+ i = shortcut_index(value) # i is between 1 and 99 or None
309
+ if i is not None:
310
+ if not self.playground_scenarios:
311
+ print(f"Unknown scenario: {value}")
312
+ self.show_scenarios()
313
+ return
314
+ if i > len(self.playground_scenarios):
315
+ print(f"Undefined scenario shortcut: [{value[1:]}]")
316
+ return
317
+ scenario_name = self.playground_scenarios[i - 1] # User counts items starting from 1
318
+ else:
319
+ scenario_name = value
320
+ if scenario_name not in self.playground_scenarios:
321
+ print(f"Unknown scenario: {scenario_name}")
322
+ return
323
+ print(f"Selected scenario: {scenario_name}")
324
+
325
+ sfile = self.system.playground / 'scenarios' / (scenario_name + ".yaml")
326
+ self.active_scenario = Scenario(sfile)
327
+
328
+ def show_scenarios(self) -> None:
329
+ """
330
+ Display all scenarios defined in the active playground directory
331
+ with convenient integer shortcuts
332
+ """
333
+ scenario_path = self.system.playground / 'scenarios'
334
+ scenario_paths = list(scenario_path.glob("*.yaml"))
335
+ self.playground_scenarios = [f.stem for f in scenario_paths]
336
+ if self.playground_scenarios:
337
+ print("Active playground scenarios:")
338
+ for n, p in enumerate(self.playground_scenarios):
339
+ print(f" [{n + 1}] - {p}")
340
+
341
+ @staticmethod
342
+ def format_interaction(i: Interaction):
343
+ if i.action == ActionType.SIGNAL_INSTANCE:
344
+ inst_str = '<' + '-'.join([str(v) for v in i.target.instance_id.values()]) + '>'
345
+ formatted_i = f"{i.source.domain} >|| {i.target.domain} : {i.name} -> {i.target.class_name} {inst_str}"
346
+ else:
347
+ formatted_i = "Unimplemented Acton Type"
348
+ print(formatted_i)
349
+
350
+ @staticmethod
351
+ def format_announcements(announcement_tuples: list[Announcement]):
352
+ for a in announcement_tuples:
353
+ if isinstance(a, ExternalEvent_Announcement):
354
+ if a.inst:
355
+ inst_str = '<' + '-'.join([str(v) for v in a.inst.values()]) + '>'
356
+ else:
357
+ inst_str = ""
358
+ pstrings = [f"{n}={v[0]}" for n,v in a.params.items()]
359
+ param_str = ', '.join(pstrings)
360
+ formatted_a = f"{a.domain} >|| {a.ee} : {a.source}{inst_str} {a.event}( {param_str} )"
361
+ print(f" {formatted_a}")
362
+
363
+ def execute_scenario(self) -> None:
364
+ """
365
+ Process each interaction of the scenario until it is complete
366
+ """
367
+ print(f"Running scenario: {self.scenario_name}...\n")
368
+ # Set external events to trigger an announcement and return control after enclosing activity completes
369
+ System.set_announce_triggers(['external signal'])
370
+
371
+ interactions = iter(self.active_scenario.interactions)
372
+ i = next(interactions, None)
373
+
374
+ while i is not None:
375
+ if self.descriptions:
376
+ print(f" ** {i.description} ** ")
377
+ # Process the current interaction
378
+ if i.direction == Direction.STIMULUS:
379
+ Session.format_interaction(i) # Print out the stimulus injection info
380
+ self.system.inject(stimulus=i) # Inject the stimulus, transferring control to MX
381
+ self.format_announcements(self.system.announcements) # Print out any triggered announcments
382
+
383
+ # TODO: Think about how to correlate announcements and responses
384
+ # The triggered announcments should correspond to the expedted response interactions
385
+ # but at this point, we doin't attempt to match them up. This may be in the realm of a testing
386
+ # package, but we keep them in the scenario yaml for now.
387
+ else: # Response
388
+ self.system.go() # No stimulus to inject, so just pass control back to MX
389
+ self.format_announcements(self.system.announcements) # Print out any triggered announcments
390
+
391
+ if not self.stepping:
392
+ i = next(interactions, None)
393
+ continue
394
+
395
+ # Inner pause loop: let user inspect results before stepping forward
396
+ while True:
397
+ try:
398
+ raw = input(STEP_PROMPT).strip().lower()
399
+ except (EOFError, KeyboardInterrupt):
400
+ print("\nScenario aborted.")
401
+ return
402
+
403
+ match raw:
404
+ case "" | "n" | "next" | "s" | "step":
405
+ i = next(interactions, None) # Advance to the next interaction
406
+ break
407
+ case "r" | "run":
408
+ self.stepping = False # Run to completion
409
+ i = next(interactions, None)
410
+ break
411
+ case "q" | "quit" | "abort":
412
+ print("Scenario aborted.")
413
+ return
414
+ case "show states":
415
+ self.show_states()
416
+ case "h" | "help" | "?":
417
+ print(" [enter] / n / next / s / step - Advance to next interaction")
418
+ print(" show states - Display current state of all state models")
419
+ print(" q / quit / abort - Abort scenario")
420
+ case _:
421
+ print(f"Unknown step command: '{raw}'. Type 'h' for help.")
422
+
423
+ print("\nScenario complete.")
@@ -0,0 +1,4 @@
1
+ """ ui_types.py -- Characters and messages used in the command line interface """
2
+
3
+ CMD_PROMPT = "# "
4
+ STATUS = "..."
@@ -0,0 +1,49 @@
1
+ Metadata-Version: 2.4
2
+ Name: mi-mdb
3
+ Version: 0.1.0
4
+ Summary: Shlaer-Mellor Executable UML Model Debugger
5
+ Author-email: Leon Starr <leon_starr@modelint.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Leon Starr
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: repository, https://github.com/modelint/model-debugger
29
+ Project-URL: documentation, https://github.com/modelint/model-debugger/wiki
30
+ Keywords: shlaer-mellor,metamodel,executable uml,mbse,xuml,xtuml,model debugger,platform independent
31
+ Classifier: License :: OSI Approved :: MIT License
32
+ Classifier: Programming Language :: Python
33
+ Classifier: Programming Language :: Python :: 3
34
+ Requires-Python: >=3.11
35
+ Description-Content-Type: text/markdown
36
+ License-File: LICENSE
37
+ Requires-Dist: mi-mx
38
+ Requires-Dist: mi-pyral
39
+ Requires-Dist: PyYAML
40
+ Provides-Extra: build
41
+ Requires-Dist: build; extra == "build"
42
+ Requires-Dist: twine; extra == "build"
43
+ Provides-Extra: dev
44
+ Requires-Dist: bump2version; extra == "dev"
45
+ Requires-Dist: pytest; extra == "dev"
46
+ Dynamic: license-file
47
+
48
+ # model-debugger
49
+ A modeling language independent model debugger
@@ -0,0 +1,17 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.md
4
+ pyproject.toml
5
+ src/mdb/__init__.py
6
+ src/mdb/__main__.py
7
+ src/mdb/_dep_system.py
8
+ src/mdb/log.conf
9
+ src/mdb/scenario.py
10
+ src/mdb/session.py
11
+ src/mdb/ui_types.py
12
+ src/mi_mdb.egg-info/PKG-INFO
13
+ src/mi_mdb.egg-info/SOURCES.txt
14
+ src/mi_mdb.egg-info/dependency_links.txt
15
+ src/mi_mdb.egg-info/entry_points.txt
16
+ src/mi_mdb.egg-info/requires.txt
17
+ src/mi_mdb.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mdb = mdb.__main__:main
@@ -0,0 +1,11 @@
1
+ mi-mx
2
+ mi-pyral
3
+ PyYAML
4
+
5
+ [build]
6
+ build
7
+ twine
8
+
9
+ [dev]
10
+ bump2version
11
+ pytest
@@ -0,0 +1 @@
1
+ mdb