cvdlink 0.1.1__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 (45) hide show
  1. FeatureCloud/__init__.py +0 -0
  2. FeatureCloud/api/__init__.py +0 -0
  3. FeatureCloud/api/cli/__init__.py +0 -0
  4. FeatureCloud/api/cli/__main__.py +115 -0
  5. FeatureCloud/api/cli/app/__init__.py +0 -0
  6. FeatureCloud/api/cli/app/commands.py +182 -0
  7. FeatureCloud/api/cli/controller/__init__.py +0 -0
  8. FeatureCloud/api/cli/controller/commands.py +181 -0
  9. FeatureCloud/api/cli/test/__init__.py +0 -0
  10. FeatureCloud/api/cli/test/commands.py +251 -0
  11. FeatureCloud/api/cli/test/workflow/__init__.py +0 -0
  12. FeatureCloud/api/cli/test/workflow/commands.py +32 -0
  13. FeatureCloud/api/imp/__init__.py +0 -0
  14. FeatureCloud/api/imp/app/__init__.py +0 -0
  15. FeatureCloud/api/imp/app/commands.py +278 -0
  16. FeatureCloud/api/imp/controller/__init__.py +0 -0
  17. FeatureCloud/api/imp/controller/commands.py +246 -0
  18. FeatureCloud/api/imp/exceptions.py +29 -0
  19. FeatureCloud/api/imp/test/__init__.py +0 -0
  20. FeatureCloud/api/imp/test/api/__init__.py +0 -0
  21. FeatureCloud/api/imp/test/api/backend/__init__.py +0 -0
  22. FeatureCloud/api/imp/test/api/backend/auth.py +54 -0
  23. FeatureCloud/api/imp/test/api/backend/project.py +84 -0
  24. FeatureCloud/api/imp/test/api/controller.py +97 -0
  25. FeatureCloud/api/imp/test/commands.py +124 -0
  26. FeatureCloud/api/imp/test/helper.py +40 -0
  27. FeatureCloud/api/imp/util.py +45 -0
  28. FeatureCloud/app/__init__.py +0 -0
  29. FeatureCloud/app/api/__init__.py +0 -0
  30. FeatureCloud/app/api/http_ctrl.py +48 -0
  31. FeatureCloud/app/api/http_web.py +16 -0
  32. FeatureCloud/app/engine/__init__.py +0 -0
  33. FeatureCloud/app/engine/app.py +1214 -0
  34. FeatureCloud/app/engine/library.py +46 -0
  35. FeatureCloud/workflow/__init__.py +0 -0
  36. FeatureCloud/workflow/app.py +197 -0
  37. FeatureCloud/workflow/controller.py +17 -0
  38. FeatureCloud/workflow/example_wf.py +83 -0
  39. FeatureCloud/workflow/workflow.py +86 -0
  40. cvdlink-0.1.1.dist-info/METADATA +176 -0
  41. cvdlink-0.1.1.dist-info/RECORD +45 -0
  42. cvdlink-0.1.1.dist-info/WHEEL +5 -0
  43. cvdlink-0.1.1.dist-info/entry_points.txt +5 -0
  44. cvdlink-0.1.1.dist-info/licenses/LICENSE +201 -0
  45. cvdlink-0.1.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,251 @@
1
+ import os
2
+ import time
3
+
4
+ import click
5
+ import requests
6
+
7
+ from FeatureCloud.api.imp.exceptions import FCException
8
+
9
+ from FeatureCloud.api.imp.test import commands
10
+ from FeatureCloud.api.cli.test.workflow.commands import workflow
11
+ from FeatureCloud.api.imp.test.helper import http
12
+
13
+
14
+ @click.group("test")
15
+ def test() -> None:
16
+ """Testbed related commands"""
17
+
18
+
19
+ test.add_command(workflow)
20
+
21
+
22
+ @test.command('help')
23
+ def help():
24
+ _, msg = commands.help()
25
+ click.echo(msg)
26
+
27
+
28
+ @test.command('start')
29
+ @click.option('--controller-host', default='http://localhost:8000',
30
+ help='Address of your running controller instance (e.g. featurecloud test start --controller-host=http://localhost:8000).')
31
+ @click.option('--client-dirs', default='.,.',
32
+ help='Client directories separated by comma. The number of clients is based on the number of directories supplied here (e.g. `featurecloud test start --client-dirs=.,.,.,.` command will start 4 clients).',
33
+ required=True)
34
+ @click.option('--generic-dir', default='.',
35
+ help='Generic directory available for all clients. Content will be copied to the input folder of all '
36
+ 'instances (e.g. featurecloud test start --generic-dir=.).',
37
+ required=True)
38
+ @click.option('--app-image', default='test_app',
39
+ help='The repository url of the app image (e.g. featurecloud test start --app-image=featurecloud.ai/test_app).',
40
+ required=True)
41
+ @click.option('--channel', default='local',
42
+ help='The communication channel to be used. Possible values: "local" or "internet" (e.g. featurecloud test start --channel=local).')
43
+ @click.option('--query-interval', default=2.0,
44
+ help='The interval after how many seconds the status call will be performed (e.g. featurecloud test start --query-interval=2).')
45
+ @click.option('--download-results',
46
+ help='A directory name where to download results. This will be created into /data/tests directory (e.g. featurecloud test start --download-results=./results).',
47
+ default='')
48
+ @click.option('--print-logs',
49
+ help='When selected, it will monitor the started test by printing the current status every 3s. When the test is finished (or has an error), the relevant logs will be output. Canceling the command after the test was started will NOT stop the test.',
50
+ is_flag=True)
51
+ def start(controller_host: str, client_dirs: str, generic_dir: str, app_image: str, channel: str, query_interval: str,
52
+ download_results: str, print_logs: bool):
53
+ """Starts testbed run with the specified parameters"""
54
+ try:
55
+ test_id = commands.start(controller_host, client_dirs, generic_dir, app_image, channel, query_interval,
56
+ download_results)
57
+ click.echo(f"Test id={test_id} started")
58
+ except requests.exceptions.InvalidSchema:
59
+ click.echo(f'No connection adapters were found for {controller_host}')
60
+ return
61
+ except requests.exceptions.MissingSchema:
62
+ click.echo(f' Invalid URL {controller_host}: No scheme supplied. Perhaps you meant http://{controller_host}?')
63
+ return
64
+ except FCException as e:
65
+ click.echo(f'Error: {e}')
66
+ return
67
+ # start monitoring in case wanted (option --print-logs)
68
+ if print_logs:
69
+ while True:
70
+ time.sleep(5)
71
+ info = commands.info(controller_host, test_id)
72
+ status = info['status']
73
+ if len(status) != 1:
74
+ click.echo('monitoring failed, more than one status of the test found. Please check manually using featurecloud test logs')
75
+ return
76
+ status = status.iloc[0].strip()
77
+ # change format of starttime to be the same than used in controller logs
78
+ starttime = info['createdAt'].iloc[0].strip()
79
+ starttime = starttime[:10] + 'T' + starttime[11:]
80
+ starttime += 'Z'
81
+ click.echo(f'current Status: {status}')
82
+ if status in ['error', 'stopped', 'finished']:
83
+ # controller logs
84
+ response = http.get(url=f'{controller_host}/logs/?from={0}')
85
+ controllerString = "CONTROLLER"
86
+ click.echo(controllerString)
87
+ click.echo("="*len(controllerString))
88
+ if response.status_code == 200:
89
+ contlogs = response.json()
90
+ for logentry in contlogs:
91
+ timeentry = logentry['time']
92
+ if timeentry >= starttime:
93
+ click.echo(logentry)
94
+ click.echo('\n\n')
95
+ else:
96
+ click.echo("Could not get controller logs, use the frontend or featurecloud controller logs")
97
+ # logs of all clients
98
+ instances = commands.info(controller_host, test_id)['instances']
99
+ for inst in instances.iloc[0]:
100
+ inst_id = inst['id']
101
+ logs = commands.logs(controller_host, test_id, inst_id, '0')
102
+ clientString = f"CLIENT NUMBER {inst_id}"
103
+ click.echo(clientString)
104
+ click.echo('='*len(clientString))
105
+ click.echo('\n'.join(logs[-50:]) + '\n\n') # only show last 50 lines
106
+ return
107
+ elif status in ['running', 'writing']:
108
+ continue
109
+ else:
110
+ click.echo('Unknown status, stopping monitoring. Please check manually using featurecloud test logs or with the frontend')
111
+ return
112
+
113
+
114
+
115
+ @test.command('stop')
116
+ @click.option('--controller-host', default='http://localhost:8000',
117
+ help='Http address of your running controller instance (e.g. featurecloud test stop --controller-host=http://localhost:8000).',
118
+ required=True)
119
+ @click.option('--test-id', help='The test id of the test to be stopped. The test id is returned by the start command (e.g.featurecloud test stop --test-id=1).')
120
+ def stop(controller_host: str, test_id: str or int):
121
+ '''Stops test with specified test id'''
122
+ try:
123
+ result = commands.stop(controller_host, test_id)
124
+ click.echo(f"Test id={result} stopped")
125
+ except requests.exceptions.InvalidSchema:
126
+ click.echo(f'No connection adapters were found for {controller_host}')
127
+ except requests.exceptions.MissingSchema:
128
+ click.echo(f' Invalid URL {controller_host}: No scheme supplied. Perhaps you meant http://{controller_host}?')
129
+ except FCException as e:
130
+ click.echo(f'Error: {e}')
131
+
132
+
133
+ @test.command('delete')
134
+ @click.option('--controller-host', default='http://localhost:8000',
135
+ help='Address of your running controller instance. (e.g. featurecloud test delete all --controller-host=http://localhost:8000)',)
136
+ @click.option('--test-id', help='The test id of the test to be deleted. The test id is returned by the start command.'
137
+ 'To delete all tests omit this option and use "delete all".')
138
+ @click.argument('all', type=str, nargs=1, required=False)
139
+ def delete(controller_host: str, test_id: str or int, all: str):
140
+ '''
141
+ Deletes test with specified id or alternatively, deletes all tests
142
+
143
+ ALL - delete all tests
144
+
145
+ Examples:
146
+
147
+ featurecloud test delete --test-id=1
148
+
149
+ featurecloud test delete all
150
+ '''
151
+ try:
152
+ result = commands.delete(controller_host, test_id, all)
153
+ if all is not None:
154
+ if all.lower() == 'all':
155
+ click.echo(f"All tests deleted")
156
+ else:
157
+ click.echo(f'Wrong parameter {all}')
158
+ else:
159
+ click.echo(f"Test id={result} deleted")
160
+ except requests.exceptions.InvalidSchema:
161
+ click.echo(f'No connection adapters were found for {controller_host}')
162
+ except requests.exceptions.MissingSchema:
163
+ click.echo(f' Invalid URL {controller_host}: No scheme supplied. Perhaps you meant http://{controller_host}?')
164
+ except FCException as e:
165
+ click.echo(f'Error: {e}')
166
+
167
+
168
+ @test.command('list')
169
+ @click.option('--controller-host', default='http://localhost:8000',
170
+ help='Address of your running controller instance (e.g. featurecloud test list --controller-host=http://localhost:8000).',
171
+ required=True)
172
+ @click.option('--format', help='Format of the test list. Possible options: json or dataframe (e.g. featurecloud test list --format=dataframe).', required=True, default='dataframe')
173
+ def list(controller_host: str, format: str):
174
+ '''List all tests'''
175
+ try:
176
+ result = commands.list(controller_host, format)
177
+ if len(result) == 0:
178
+ click.echo('No tests available')
179
+ else:
180
+ click.echo(result)
181
+ except requests.exceptions.InvalidSchema:
182
+ click.echo(f'No connection adapters were found for {controller_host}')
183
+ except requests.exceptions.MissingSchema:
184
+ click.echo(f' Invalid URL {controller_host}: No scheme supplied. Perhaps you meant http://{controller_host}?')
185
+ except FCException as e:
186
+ click.echo(f'Error: {e}')
187
+
188
+
189
+ @test.command('info')
190
+ @click.option('--controller-host', default='http://localhost:8000',
191
+ help='Address of your running controller instance (e.g. featurecloud test info --controller-host=http://localhost:8000).',
192
+ required=True)
193
+ @click.option('--test-id', help='Test id to get info about (e.g. featurecloud test info --test-id=1).', required=True)
194
+ @click.option('--format', help='Format of the test info. Possible values: json or dataframe (e.g. featurecloud test info --format=dataframe).', required=True, default='dataframe')
195
+ def info(controller_host: str, test_id: str or int, format: str):
196
+ '''Get information about a running test'''
197
+ try:
198
+ result = commands.info(controller_host, test_id, format)
199
+ click.echo(result)
200
+ except requests.exceptions.InvalidSchema:
201
+ click.echo(f'No connection adapters were found for {controller_host}')
202
+ except requests.exceptions.MissingSchema:
203
+ click.echo(f' Invalid URL {controller_host}: No scheme supplied. Perhaps you meant http://{controller_host}?')
204
+ except FCException as e:
205
+ click.echo(f'Error: {e}')
206
+
207
+
208
+ @test.command('traffic')
209
+ @click.option('--controller-host', default='http://localhost:8000',
210
+ help='Address of your running controller instance (e.g. featurecloud test traffic --controller-host=http://localhost:8000).',
211
+ required=True)
212
+ @click.option('--test-id', help='The test id to get traffic info about (e.g. featurecloud test traffic --test-id=1).')
213
+ @click.option('--format', help='Format of the test traffic. Possible values: json or dataframe (e.g. featurecloud test traffic --format=dataframe).e', required=True, default='dataframe')
214
+ def traffic(controller_host: str, test_id: str or int, format: str):
215
+ '''Displays traffic information inside tests'''
216
+ try:
217
+ result = commands.traffic(controller_host, test_id, format)
218
+ click.echo(result)
219
+ except requests.exceptions.InvalidSchema:
220
+ click.echo(f'No connection adapters were found for {controller_host}')
221
+ except requests.exceptions.MissingSchema:
222
+ click.echo(f' Invalid URL {controller_host}: No scheme supplied. Perhaps you meant http://{controller_host}?')
223
+ except FCException as e:
224
+ click.echo(f'Error: {e}')
225
+
226
+
227
+ @test.command('logs')
228
+ @click.option('--controller-host', default='http://localhost:8000',
229
+ help='Address of your running controller instance (e.g. featurecloud test logs --controller-host=http://localhost:8000).',
230
+ required=True)
231
+ @click.option('--test-id', help='The test id to get logs about (e.g. featurecloud test logs --test-id=1).', required=True)
232
+ @click.option('--instance-id', help='The instance id of the test client. Instance ids can be obtained by running the info command (e.g. featurecloud test logs --test-id=1 --instance-id=0).', required=True)
233
+ @click.option('--from-row', help='Get logs from a certain row number (e.g. featurecloud test logs --test-id=1 --instance-id=0 --from-row=0).', default='', required=True)
234
+ def logs(controller_host: str, test_id: str or int, instance_id: str or int, from_row: str):
235
+ '''Get logs from test client'''
236
+ try:
237
+ result = commands.logs(controller_host, test_id, instance_id, from_row)
238
+ log_lines = ""
239
+ for line in result:
240
+ log_lines += str(line) + os.linesep
241
+ click.echo(log_lines)
242
+ except requests.exceptions.InvalidSchema:
243
+ click.echo(f'No connection adapters were found for {controller_host}')
244
+ except requests.exceptions.MissingSchema:
245
+ click.echo(f' Invalid URL {controller_host}: No scheme supplied. Perhaps you meant http://{controller_host}?')
246
+ except FCException as e:
247
+ click.echo(f'Error: {e}')
248
+
249
+
250
+ if __name__ == "__main__":
251
+ test()
File without changes
@@ -0,0 +1,32 @@
1
+ import click
2
+ import importlib
3
+ import sys
4
+
5
+
6
+ @click.group("workflow")
7
+ def workflow() -> None:
8
+ """Workflow related commands"""
9
+
10
+
11
+ @workflow.command('start')
12
+ @click.option('--controller-host', default='http://localhost:8000',
13
+ help='Address of your running controller instance.',
14
+ required=True)
15
+ @click.option('--wf-dir', default='',
16
+ help='path to directory containing the workflow',
17
+ required=True)
18
+ @click.option('--wf-file', default='example_wf',
19
+ help='python file including the workflow',
20
+ required=True)
21
+ @click.option('--channel', default='local',
22
+ help='The communication channel to be used. Can be local or internet.',
23
+ required=True)
24
+ @click.option('--query-interval', default=1,
25
+ help='The interval after how many seconds the status call will be performed.',
26
+ required=True)
27
+ def start_workflow(controller_host: str, wf_dir: str, wf_file: str, channel: str, query_interval: str):
28
+ sys.path.append(wf_dir)
29
+ workflow_class = importlib.import_module(wf_file)
30
+ wf = workflow_class.WorkFlow(controller_host=controller_host, channel=channel, query_interval=query_interval)
31
+ wf.register_apps()
32
+ wf.run()
File without changes
File without changes
@@ -0,0 +1,278 @@
1
+ import git
2
+ import os
3
+ from urllib.parse import urljoin
4
+ import docker
5
+ from docker import DockerClient
6
+ import sys
7
+ from git import GitError
8
+
9
+ from FeatureCloud.api.imp.exceptions import FCException
10
+ from FeatureCloud.api.imp.util import getcwd_fslash, get_docker_client, remove_dir
11
+ import pydot
12
+ import importlib
13
+
14
+ FC_REPO_PREFIX = "featurecloud.ai/"
15
+
16
+
17
+ def create_link(template_name: str) -> str:
18
+ TEMPLATE_URL = 'https://github.com/FeatureCloud/'
19
+ return urljoin(TEMPLATE_URL, template_name + '.git')
20
+
21
+
22
+ def new(name: str, directory: str = '.', template_name: str = 'app-blank') -> str:
23
+ """
24
+ Parameters
25
+ ----------
26
+ name: str
27
+ app name
28
+ directory: str
29
+ path to create the app's directory in
30
+ template_name: str
31
+ a template repository in FeatureCloud repositories
32
+ Returns
33
+ -------
34
+ str
35
+ path to the created app
36
+ Raises
37
+ -------
38
+ FeatureCloud.api.imp.exceptions.FCException
39
+ """
40
+
41
+ if not template_name.startswith('app-'):
42
+ raise FCException(f'{template_name} is not a valid template. Please see the help for available templates')
43
+
44
+ try:
45
+ app_path = os.path.join(directory, name.lower())
46
+ # depth=1 clones with minimal history information, we delete the .git folder anyway
47
+ repo = git.Repo.clone_from(create_link(template_name), app_path, multi_options=['--depth=1'])
48
+ repo.delete_remote('origin')
49
+ except GitError as e:
50
+ raise FCException(e)
51
+
52
+ # delete the .git folder
53
+ try:
54
+ remove_dir(os.path.join(app_path, '.git'))
55
+ except:
56
+ pass
57
+
58
+ return app_path
59
+
60
+
61
+ def build(path: str = ".", image_name: str = None, tag: str = "latest", rm: str = True):
62
+ """ Build app image.
63
+
64
+ Parameters
65
+ ----------
66
+ path: str
67
+ path to the directory containing the Dockerfile
68
+ image_name: str
69
+ name of the app's docker image. If not provided, the name of the current directory will be used.
70
+ tag: str
71
+ versioning tag
72
+ rm: bool
73
+ if True, remove intermediate containers
74
+ Returns
75
+ -------
76
+ lines
77
+ generator providing information about the build progress
78
+ Raises
79
+ -------
80
+ FeatureCloud.api.imp.exceptions.FCException
81
+ """
82
+ if image_name is None:
83
+ image_name = getcwd_fslash().split("/")[-1]
84
+
85
+ client = get_docker_client()
86
+ try:
87
+ # Docker would also check for Dockerfile, but if, by accident, a path is provided with many sub folders
88
+ # it just takes too long. So we check it upfront:
89
+ if not os.path.exists(os.path.join(path, 'Dockerfile')):
90
+ raise FCException(f'Dockerfile not found in directory: {os.path.abspath(path)}')
91
+ build_log_generator = client.images.build(path=path, tag=f"{image_name.lower()}:{tag}", rm=rm)
92
+ if type(build_log_generator) == tuple:
93
+ return build_log_generator[1]
94
+ else:
95
+ raise Exception("Unexpected response from the docker SDK")
96
+ except docker.errors.BuildError as e:
97
+ raise FCException("Error in building the image: " + str(e))
98
+ except docker.errors.APIError as e:
99
+ raise FCException("Error with the API: " + str(e))
100
+ except TypeError as e:
101
+ raise FCException("Error path to the Dockerfile not given: " + str(e))
102
+
103
+
104
+ def download(name: str, tag: str = "latest"):
105
+ """ Download a given docker image from FeatureCloud.ai docker repo.
106
+
107
+ Parameters
108
+ ----------
109
+ name: str
110
+ image name
111
+ tag: str
112
+ versioning tag
113
+ Returns
114
+ -------
115
+ lines
116
+ generator providing information about the download progress
117
+ Raises
118
+ -------
119
+ FeatureCloud.api.imp.exceptions.FCException
120
+ """
121
+ fc_name = fc_repo_name(name)
122
+ client = get_docker_client()
123
+ try:
124
+ for entry in client.api.pull(repository=fc_name, tag=tag, stream=True, decode=True):
125
+ if 'error' in entry:
126
+ raise FCException(entry['error'])
127
+
128
+ yield entry
129
+ except docker.errors.DockerException as e:
130
+ raise FCException(e)
131
+
132
+
133
+ def publish(name: str, tag: str = "latest"):
134
+ """ Push a given app into FeatureCloud.ai docker repo.
135
+
136
+ Parameters
137
+ ----------
138
+ name: str
139
+ image name
140
+ tag: str
141
+ versioning tag
142
+ Returns
143
+ -------
144
+ lines
145
+ generator providing information about the publish progress
146
+ Raises
147
+ -------
148
+ FeatureCloud.api.imp.exceptions.FCException
149
+ """
150
+ fc_name = fc_repo_name(name)
151
+ client = get_docker_client()
152
+
153
+ try:
154
+ local_repo = f'{name}:{tag}'
155
+ registry_repo = f'{fc_name}:{tag}'
156
+ client.images.get(local_repo).tag(registry_repo)
157
+ except docker.errors.DockerException as e:
158
+ raise FCException(e)
159
+
160
+ try:
161
+ for entry in client.api.push(repository=registry_repo, stream=True, decode=True):
162
+ # Examples of stream entries:
163
+ # {'status': 'Waiting', 'progressDetail': {}, 'id': '38feb0548c7a'}
164
+ # {'errorDetail': {'message': 'unauthorized: authentication required'}, 'error': 'unauthorized: authentication required'}
165
+ if 'error' in entry:
166
+ raise FCException(entry['error'])
167
+
168
+ yield entry
169
+ except docker.errors.DockerException as e:
170
+ raise FCException(e)
171
+
172
+
173
+ def remove(name: str, tag: str = "latest"):
174
+ """ Delete docker image from local hard drive.
175
+
176
+ Parameters
177
+ ----------
178
+ name: str
179
+ image name
180
+ tag: str
181
+ the versioning tag to be removed. If set to 'all', all versions will be deleted.
182
+ Raises
183
+ -------
184
+ FeatureCloud.api.imp.exceptions.FCException
185
+ """
186
+ to_find = name if tag == 'all' else f'{name}:{tag}'
187
+ client = get_docker_client()
188
+ removed = remove_images(client, to_find)
189
+
190
+ if tag == 'all':
191
+ # search also for local images containing the FC repository name
192
+ to_find = fc_repo_name(name)
193
+ removed = removed + remove_images(client, to_find)
194
+
195
+ return removed
196
+
197
+
198
+ def fc_repo_name(name: str) -> str:
199
+ if not name.startswith(FC_REPO_PREFIX):
200
+ return f'{FC_REPO_PREFIX}{name}'
201
+
202
+ return name
203
+
204
+
205
+ def remove_images(client: DockerClient, to_find: str):
206
+ removed = []
207
+ for img in client.images.list(to_find):
208
+ # one image id may have several tags
209
+ for img_tag in img.tags:
210
+ try:
211
+ client.images.remove(image=img_tag, force=True)
212
+ except docker.errors.DockerException as e:
213
+ raise FCException(e)
214
+ removed = removed + img.tags
215
+
216
+ return removed
217
+
218
+
219
+ def plot_state_diagram(path: str, package: str, states: str, plot_name: str):
220
+ states = list(filter(None, states.split(',')))
221
+ sys.path.append(path)
222
+ if "/" in package:
223
+ sub_pkg_path = path
224
+ for sub_pkg in package.split("/"):
225
+ sub_pkg_path += "/" + sub_pkg
226
+ sys.path.append(sub_pkg_path)
227
+ else:
228
+ sys.path.append(f"{path}/{package}")
229
+ if states == 'main':
230
+ from FeatureCloud.app.engine.app import app
231
+ main_py = load_module(states[0], f"{path}/{package}/{states[0]}.py")
232
+ app = main_py.app
233
+
234
+ else:
235
+ for state in states:
236
+ load_module(state, f"{path}/{package}/{state}.py")
237
+ from FeatureCloud.app.engine.app import app
238
+
239
+
240
+ app.register()
241
+
242
+ graph = pydot.Dot('FeatureCloud State Diagram', graph_type='digraph', bgcolor='transparent')
243
+
244
+ for s in app.states:
245
+ state = app.states[s]
246
+ state_node = pydot.Node(state.name, label=state.name)
247
+ if state.coordinator and state.participant:
248
+ state_node.set('color', 'purple')
249
+ elif state.coordinator:
250
+ state_node.set('color', 'red')
251
+ elif state.participant:
252
+ state_node.set('color', 'blue')
253
+ if state.name == 'initial' or state.name == 'terminal':
254
+ state_node.set('peripheries', 2)
255
+ graph.add_node(state_node)
256
+
257
+ for t in app.transitions:
258
+ transition = app.transitions[t]
259
+ label = t if transition[4] is None else transition[4]
260
+
261
+ state_edge = pydot.Edge(transition[0].name, transition[1].name, label=label)
262
+ if transition[2] and transition[3]:
263
+ state_edge.set('color', 'purple')
264
+ elif transition[3]:
265
+ state_edge.set('color', 'red')
266
+ elif transition[2]:
267
+ state_edge.set('color', 'blue')
268
+ graph.add_edge(state_edge)
269
+
270
+ graph.write_png(f"{path}/{plot_name}.png")
271
+
272
+
273
+ def load_module(module, path):
274
+ loader = importlib.machinery.SourceFileLoader(module, path)
275
+ spec = importlib.util.spec_from_loader(module, loader)
276
+ mymodule = importlib.util.module_from_spec(spec)
277
+ loader.exec_module(mymodule)
278
+ return mymodule
File without changes