stoobly-agent 0.34.13__py3-none-any.whl → 1.0.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 (132) hide show
  1. stoobly_agent/__init__.py +1 -1
  2. stoobly_agent/app/cli/__init__.py +1 -0
  3. stoobly_agent/app/cli/helpers/openapi_endpoint_adapter.py +9 -7
  4. stoobly_agent/app/cli/helpers/shell.py +28 -0
  5. stoobly_agent/app/cli/main_group.py +1 -1
  6. stoobly_agent/app/cli/scaffold/__init__.py +0 -0
  7. stoobly_agent/app/cli/scaffold/app.py +106 -0
  8. stoobly_agent/app/cli/scaffold/app_command.py +82 -0
  9. stoobly_agent/app/cli/scaffold/app_config.py +32 -0
  10. stoobly_agent/app/cli/scaffold/app_create_command.py +24 -0
  11. stoobly_agent/app/cli/scaffold/command.py +15 -0
  12. stoobly_agent/app/cli/scaffold/config.py +35 -0
  13. stoobly_agent/app/cli/scaffold/constants.py +35 -0
  14. stoobly_agent/app/cli/scaffold/docker/__init__.py +0 -0
  15. stoobly_agent/app/cli/scaffold/docker/app_builder.py +26 -0
  16. stoobly_agent/app/cli/scaffold/docker/builder.py +117 -0
  17. stoobly_agent/app/cli/scaffold/docker/constants.py +7 -0
  18. stoobly_agent/app/cli/scaffold/docker/service/__init__.py +0 -0
  19. stoobly_agent/app/cli/scaffold/docker/service/build_decorator.py +37 -0
  20. stoobly_agent/app/cli/scaffold/docker/service/builder.py +117 -0
  21. stoobly_agent/app/cli/scaffold/docker/service/set_gateway_ports.py +47 -0
  22. stoobly_agent/app/cli/scaffold/docker/service/types.py +4 -0
  23. stoobly_agent/app/cli/scaffold/docker/workflow/__init__.py +0 -0
  24. stoobly_agent/app/cli/scaffold/docker/workflow/build_decorator.py +28 -0
  25. stoobly_agent/app/cli/scaffold/docker/workflow/builder.py +259 -0
  26. stoobly_agent/app/cli/scaffold/docker/workflow/decorators_factory.py +17 -0
  27. stoobly_agent/app/cli/scaffold/docker/workflow/mock_decorator.py +40 -0
  28. stoobly_agent/app/cli/scaffold/docker/workflow/reverse_proxy_decorator.py +51 -0
  29. stoobly_agent/app/cli/scaffold/env.py +49 -0
  30. stoobly_agent/app/cli/scaffold/service.py +25 -0
  31. stoobly_agent/app/cli/scaffold/service_command.py +50 -0
  32. stoobly_agent/app/cli/scaffold/service_config.py +207 -0
  33. stoobly_agent/app/cli/scaffold/service_create_command.py +77 -0
  34. stoobly_agent/app/cli/scaffold/service_workflow.py +18 -0
  35. stoobly_agent/app/cli/scaffold/templates/__init__.py +0 -0
  36. stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.context +8 -0
  37. stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.proxy +35 -0
  38. stoobly_agent/app/cli/scaffold/templates/app/.Makefile +118 -0
  39. stoobly_agent/app/cli/scaffold/templates/app/.docker-compose.base.yml +15 -0
  40. stoobly_agent/app/cli/scaffold/templates/app/Makefile +3 -0
  41. stoobly_agent/app/cli/scaffold/templates/app/build/.config.yml +1 -0
  42. stoobly_agent/app/cli/scaffold/templates/app/build/.docker-compose.base.yml +11 -0
  43. stoobly_agent/app/cli/scaffold/templates/app/build/mock/.docker-compose.mock.yml +22 -0
  44. stoobly_agent/app/cli/scaffold/templates/app/build/mock/bin/.configure +5 -0
  45. stoobly_agent/app/cli/scaffold/templates/app/build/mock/bin/.init +5 -0
  46. stoobly_agent/app/cli/scaffold/templates/app/build/mock/bin/configure +1 -0
  47. stoobly_agent/app/cli/scaffold/templates/app/build/mock/bin/init +1 -0
  48. stoobly_agent/app/cli/scaffold/templates/app/build/record/.docker-compose.record.yml +22 -0
  49. stoobly_agent/app/cli/scaffold/templates/app/build/record/bin/.configure +5 -0
  50. stoobly_agent/app/cli/scaffold/templates/app/build/record/bin/.init +5 -0
  51. stoobly_agent/app/cli/scaffold/templates/app/build/record/bin/configure +1 -0
  52. stoobly_agent/app/cli/scaffold/templates/app/build/record/bin/init +1 -0
  53. stoobly_agent/app/cli/scaffold/templates/app/build/test/.docker-compose.test.yml +22 -0
  54. stoobly_agent/app/cli/scaffold/templates/app/build/test/bin/.configure +5 -0
  55. stoobly_agent/app/cli/scaffold/templates/app/build/test/bin/.init +5 -0
  56. stoobly_agent/app/cli/scaffold/templates/app/build/test/bin/configure +1 -0
  57. stoobly_agent/app/cli/scaffold/templates/app/build/test/bin/init +1 -0
  58. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/.config.yml +1 -0
  59. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/.docker-compose.base.yml +17 -0
  60. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/.docker-compose.mock.yml +31 -0
  61. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/bin/.configure +10 -0
  62. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/bin/.init +5 -0
  63. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/bin/configure +1 -0
  64. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/bin/init +4 -0
  65. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/docker-compose.yml +1 -0
  66. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/.docker-compose.record.yml +31 -0
  67. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/bin/.configure +10 -0
  68. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/bin/.init +5 -0
  69. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/bin/configure +1 -0
  70. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/bin/init +4 -0
  71. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/docker-compose.yml +1 -0
  72. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/.docker-compose.test.yml +31 -0
  73. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/bin/.configure +10 -0
  74. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/bin/.init +3 -0
  75. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/bin/configure +1 -0
  76. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/bin/init +4 -0
  77. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/docker-compose.yml +1 -0
  78. stoobly_agent/app/cli/scaffold/templates/app/gateway/.config.yml +1 -0
  79. stoobly_agent/app/cli/scaffold/templates/app/gateway/.docker-compose.base.yml +11 -0
  80. stoobly_agent/app/cli/scaffold/templates/app/gateway/mock/.docker-compose.mock.yml +13 -0
  81. stoobly_agent/app/cli/scaffold/templates/app/gateway/record/.docker-compose.record.yml +13 -0
  82. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/.config.yml +1 -0
  83. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/.docker-compose.base.yml +5 -0
  84. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/exec/.docker-compose.exec.yml +12 -0
  85. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/exec/bin/.create +11 -0
  86. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/exec/bin/.delete +12 -0
  87. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/exec/bin/.disable +3 -0
  88. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/exec/bin/.enable +10 -0
  89. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/exec/bin/.reset +12 -0
  90. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/exec/bin/.run +11 -0
  91. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/exec/bin/.snapshot +12 -0
  92. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/exec/bin/.stop +11 -0
  93. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/mock/.docker-compose.mock.yml +12 -0
  94. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/record/.docker-compose.record.yml +13 -0
  95. stoobly_agent/app/cli/scaffold/templates/constants.py +63 -0
  96. stoobly_agent/app/cli/scaffold/templates/factory.py +46 -0
  97. stoobly_agent/app/cli/scaffold/templates/workflow/mock/bin/.configure +3 -0
  98. stoobly_agent/app/cli/scaffold/templates/workflow/mock/bin/.init +3 -0
  99. stoobly_agent/app/cli/scaffold/templates/workflow/mock/bin/configure +18 -0
  100. stoobly_agent/app/cli/scaffold/templates/workflow/mock/bin/init +4 -0
  101. stoobly_agent/app/cli/scaffold/templates/workflow/mock/fixtures/.keep +0 -0
  102. stoobly_agent/app/cli/scaffold/templates/workflow/mock/fixtures.yml +5 -0
  103. stoobly_agent/app/cli/scaffold/templates/workflow/mock/lifecycle_hooks.py +12 -0
  104. stoobly_agent/app/cli/scaffold/templates/workflow/record/bin/.configure +3 -0
  105. stoobly_agent/app/cli/scaffold/templates/workflow/record/bin/.init +3 -0
  106. stoobly_agent/app/cli/scaffold/templates/workflow/record/bin/configure +29 -0
  107. stoobly_agent/app/cli/scaffold/templates/workflow/record/bin/init +4 -0
  108. stoobly_agent/app/cli/scaffold/templates/workflow/record/lifecycle_hooks.py +12 -0
  109. stoobly_agent/app/cli/scaffold/templates/workflow/test/bin/.configure +3 -0
  110. stoobly_agent/app/cli/scaffold/templates/workflow/test/bin/.init +3 -0
  111. stoobly_agent/app/cli/scaffold/templates/workflow/test/bin/configure +18 -0
  112. stoobly_agent/app/cli/scaffold/templates/workflow/test/bin/init +4 -0
  113. stoobly_agent/app/cli/scaffold/templates/workflow/test/fixtures/.keep +0 -0
  114. stoobly_agent/app/cli/scaffold/templates/workflow/test/fixtures.yml +5 -0
  115. stoobly_agent/app/cli/scaffold/templates/workflow/test/lifecycle_hooks.py +12 -0
  116. stoobly_agent/app/cli/scaffold/workflow.py +49 -0
  117. stoobly_agent/app/cli/scaffold/workflow_command.py +137 -0
  118. stoobly_agent/app/cli/scaffold/workflow_copy_command.py +45 -0
  119. stoobly_agent/app/cli/scaffold/workflow_create_command.py +94 -0
  120. stoobly_agent/app/cli/scaffold/workflow_log_command.py +21 -0
  121. stoobly_agent/app/cli/scaffold/workflow_run_command.py +134 -0
  122. stoobly_agent/app/cli/scaffold_cli.py +392 -0
  123. stoobly_agent/cli.py +3 -2
  124. stoobly_agent/config/data_dir.py +40 -14
  125. stoobly_agent/lib/logger.py +3 -2
  126. stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
  127. stoobly_agent/test/config/data_dir_test.py +4 -4
  128. {stoobly_agent-0.34.13.dist-info → stoobly_agent-1.0.1.dist-info}/METADATA +8 -7
  129. {stoobly_agent-0.34.13.dist-info → stoobly_agent-1.0.1.dist-info}/RECORD +132 -14
  130. {stoobly_agent-0.34.13.dist-info → stoobly_agent-1.0.1.dist-info}/WHEEL +1 -1
  131. {stoobly_agent-0.34.13.dist-info → stoobly_agent-1.0.1.dist-info}/LICENSE +0 -0
  132. {stoobly_agent-0.34.13.dist-info → stoobly_agent-1.0.1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,392 @@
1
+ import click
2
+ import os
3
+ import pdb
4
+ import sys
5
+
6
+ from typing import List
7
+
8
+ from stoobly_agent.app.cli.helpers.shell import exec_stream
9
+ from stoobly_agent.app.cli.scaffold.app import App
10
+ from stoobly_agent.app.cli.scaffold.app_create_command import AppCreateCommand
11
+ from stoobly_agent.app.cli.scaffold.constants import (
12
+ DOCKER_NAMESPACE, WORKFLOW_CUSTOM_FILTER, WORKFLOW_MOCK_TYPE, WORKFLOW_RECORD_TYPE, WORKFLOW_TEST_TYPE
13
+ )
14
+ from stoobly_agent.app.cli.scaffold.docker.service.set_gateway_ports import set_gateway_ports
15
+ from stoobly_agent.app.cli.scaffold.docker.workflow.decorators_factory import get_workflow_decorators
16
+ from stoobly_agent.app.cli.scaffold.service import Service
17
+ from stoobly_agent.app.cli.scaffold.service_config import ServiceConfig
18
+ from stoobly_agent.app.cli.scaffold.service_create_command import ServiceCreateCommand
19
+ from stoobly_agent.app.cli.scaffold.templates.constants import CORE_SERVICES
20
+ from stoobly_agent.app.cli.scaffold.workflow import Workflow
21
+ from stoobly_agent.app.cli.scaffold.workflow_create_command import WorkflowCreateCommand
22
+ from stoobly_agent.app.cli.scaffold.workflow_copy_command import WorkflowCopyCommand
23
+ from stoobly_agent.app.cli.scaffold.workflow_log_command import WorkflowLogCommand
24
+ from stoobly_agent.app.cli.scaffold.workflow_run_command import WorkflowRunCommand
25
+ from stoobly_agent.config.constants import env_vars
26
+ from stoobly_agent.config.data_dir import DataDir
27
+ from stoobly_agent.lib.logger import bcolors, DEBUG, ERROR, INFO, Logger, WARNING
28
+
29
+ LOG_ID = 'Scaffold'
30
+
31
+ @click.group(
32
+ epilog="Run 'stoobly-agent project COMMAND --help' for more information on a command.",
33
+ help="Manage scaffold"
34
+ )
35
+ @click.pass_context
36
+ def scaffold(ctx):
37
+ pass
38
+
39
+ @click.group(
40
+ epilog="Run 'stoobly-agent request response COMMAND --help' for more information on a command.",
41
+ help="Manage app scaffold"
42
+ )
43
+ @click.pass_context
44
+ def app(ctx):
45
+ pass
46
+
47
+ @click.group(
48
+ epilog="Run 'stoobly-agent request response COMMAND --help' for more information on a command.",
49
+ help="Manage service scaffold"
50
+ )
51
+ @click.pass_context
52
+ def service(ctx):
53
+ pass
54
+
55
+ @app.command(
56
+ help="Scaffold application"
57
+ )
58
+ @click.option('--app-dir-path', default=os.getcwd(), help='Path to create the app scaffold.')
59
+ @click.option('--force', is_flag=True, help='Overwrite maintained scaffolded app files.')
60
+ @click.argument('app_name')
61
+ def create(**kwargs):
62
+ __validate_app_dir(kwargs['app_dir_path'])
63
+
64
+ app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
65
+
66
+ if kwargs['force'] or not os.path.exists(app.namespace_path):
67
+ __app_build(app, **kwargs)
68
+ else:
69
+ print(f"{kwargs['app_dir_path']} already exists, use option '--force' to continue ")
70
+
71
+ def service_create_options(f):
72
+ def wrapper(*args, **kwargs):
73
+ return f(*args, **kwargs)
74
+ return wrapper
75
+
76
+ @service.command(
77
+ help="Scaffold a service",
78
+ )
79
+ @click.option('--app-dir-path', default=os.getcwd(), help='Path to application directory.')
80
+ @click.option('--detached', is_flag=True)
81
+ @click.option('--env', multiple=True, help='Specify an environment variable.')
82
+ @click.option('--force', is_flag=True, help='Overwrite maintained scaffolded service files.')
83
+ @click.option('--hostname')
84
+ @click.option('--port')
85
+ @click.option('--priority', default='5.0', help='Determines the service run order.')
86
+ @click.option('--proxy-mode', help='''
87
+ Proxy mode can be "regular", "transparent", "socks5",
88
+ "reverse:SPEC", or "upstream:SPEC". For reverse and
89
+ upstream proxy modes, SPEC is host specification in
90
+ the form of "http[s]://host[:port]".
91
+ ''')
92
+ @click.option('--scheme', type=click.Choice(['http', 'https']))
93
+ @click.option('--workflow', multiple=True, type=click.Choice([WORKFLOW_MOCK_TYPE, WORKFLOW_RECORD_TYPE, WORKFLOW_TEST_TYPE]), help='Include pre-defined workflows.')
94
+ @click.argument('service_name')
95
+ def create(**kwargs):
96
+ __validate_app_dir(kwargs['app_dir_path'])
97
+
98
+ app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
99
+
100
+ service = Service(kwargs['service_name'], app)
101
+ if kwargs['force'] or not os.path.exists(service.dir_path):
102
+ __scaffold_build(app, **kwargs)
103
+ else:
104
+ print(f"{service.dir_path} already exists, use option '--force' to continue")
105
+
106
+ @service.command(
107
+ help="Update a service config"
108
+ )
109
+ @click.option('--app-dir-path', default=os.getcwd(), help='Path to application directory.')
110
+ @click.option('--priority', help='Determines the service run order.')
111
+ @click.argument('service_name')
112
+ def update(**kwargs):
113
+ __validate_app_dir(kwargs['app_dir_path'])
114
+
115
+ app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
116
+ service = Service(kwargs['service_name'], app)
117
+
118
+ __validate_service_dir(service.dir_path)
119
+
120
+ service_config = ServiceConfig(service.dir_path)
121
+
122
+ if kwargs['priority']:
123
+ service_config.priority = kwargs['priority']
124
+
125
+ service_config.write()
126
+
127
+ @click.group(
128
+ epilog="Run 'stoobly-agent request response COMMAND --help' for more information on a command.",
129
+ help="Manage service scaffold"
130
+ )
131
+ @click.pass_context
132
+ def workflow(ctx):
133
+ pass
134
+
135
+ @workflow.command(
136
+ help="Create workflow for service(s)"
137
+ )
138
+ @click.option('--app-dir-path', default=os.getcwd(), help='Path to application directory.')
139
+ @click.option('--env', multiple=True, help='Specify an environment variable.')
140
+ @click.option('--force', is_flag=True, help='Overwrite maintained scaffolded workflow files.')
141
+ @click.option('--service', multiple=True, help='Specify the service(s) to create the workflow for.')
142
+ @click.option('--template', type=click.Choice([WORKFLOW_MOCK_TYPE, WORKFLOW_RECORD_TYPE, WORKFLOW_TEST_TYPE]), help='Select which workflow to use as a template.')
143
+ @click.argument('workflow_name')
144
+ def create(**kwargs):
145
+ __validate_app_dir(kwargs['app_dir_path'])
146
+
147
+ app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
148
+
149
+ for service_name in kwargs['service']:
150
+ config = { **kwargs }
151
+ del config['service']
152
+ config['service_name'] = service_name
153
+
154
+ service = Service(service_name, app)
155
+ __validate_service_dir(service.dir_path)
156
+
157
+ workflow_dir_path = service.workflow_dir_path(kwargs['workflow_name'])
158
+ if kwargs['force'] or not os.path.exists(workflow_dir_path):
159
+ __workflow_build(app, **config)
160
+ else:
161
+ print(f"{workflow_dir_path} already exists, use option '--force' to continue")
162
+
163
+ @workflow.command(
164
+ help="Copy a workflow for service(s)",
165
+ )
166
+ @click.option('--app-dir-path', default=os.getcwd(), help='Path to application directory.')
167
+ @click.option('--service', multiple=True, help='Specify service(s) to add the workflow to.')
168
+ @click.argument('workflow_name')
169
+ @click.argument('destination_workflow_name')
170
+ def copy(**kwargs):
171
+ app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
172
+
173
+ for service_name in kwargs['service']:
174
+ config = { **kwargs }
175
+ del config['service']
176
+ config['service_name'] = service_name
177
+
178
+ command = WorkflowCopyCommand(app, **config)
179
+
180
+ if not command.app_dir_exists:
181
+ print(f"Error: {command.app_dir_path} does not exist", file=sys.stderr)
182
+ sys.exit(1)
183
+
184
+ command.copy(kwargs['destination_workflow_name'])
185
+
186
+ @workflow.command()
187
+ @click.option('--app-dir-path', default=os.getcwd(), help='Path to application directory.')
188
+ @click.option('--context-dir-path', default=DataDir.instance().context_dir_path, help='Path to Stoobly data directory.')
189
+ @click.option('--dry-run', default=False, is_flag=True)
190
+
191
+ @click.option('--filter', multiple=True, type=click.Choice([WORKFLOW_CUSTOM_FILTER]), help='Select which service groups to run. Defaults to all.')
192
+ @click.option('--log-level', default=INFO, type=click.Choice([DEBUG, INFO, WARNING, ERROR]), help='''
193
+ Log levels can be "debug", "info", "warning", or "error"
194
+ ''')
195
+ @click.option('--service', multiple=True, help='Select which services to log. Defaults to all.')
196
+ @click.argument('workflow_name')
197
+ def stop(**kwargs):
198
+ cwd = os.getcwd()
199
+
200
+ if not os.getenv(env_vars.LOG_LEVEL):
201
+ os.environ[env_vars.LOG_LEVEL] = kwargs['log_level']
202
+
203
+ app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE, skip_validate_path=True)
204
+
205
+ if kwargs['context_dir_path']:
206
+ app.context_dir_path = kwargs['context_dir_path']
207
+
208
+ if not app.exists:
209
+ print(f"Error: {app.dir_path} does not exist", file=sys.stderr)
210
+ sys.exit(1)
211
+
212
+ workflow = Workflow(kwargs['workflow_name'], app)
213
+ services = __get_services(workflow.services, filter=kwargs['filter'], service=kwargs['service'])
214
+
215
+ commands: List[WorkflowRunCommand] = []
216
+ for service in services:
217
+ config = { **kwargs }
218
+ config['service_name'] = service
219
+ command = WorkflowRunCommand(app, **config)
220
+ command.current_working_dir = cwd
221
+ commands.append(command)
222
+
223
+ commands = sorted(commands, key=lambda command: command.service_config.priority)
224
+
225
+ for command in commands:
226
+ __print_header(f"SERVICE {command.service_name}")
227
+
228
+ exec_command = command.down()
229
+ if not exec_command:
230
+ continue
231
+
232
+ if not kwargs['dry_run']:
233
+ exec_stream(exec_command)
234
+ else:
235
+ print(exec_command)
236
+
237
+ @workflow.command()
238
+ @click.option('--app-dir-path', default=os.getcwd(), help='Path to application directory.')
239
+ @click.option('--dry-run', default=False, is_flag=True)
240
+ @click.option('--filter', multiple=True, type=click.Choice([WORKFLOW_CUSTOM_FILTER]), help='Select which service groups to run. Defaults to all.')
241
+ @click.option('--service', multiple=True, help='Select which services to log. Defaults to all.')
242
+ @click.argument('workflow_name')
243
+ def logs(**kwargs):
244
+ app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
245
+
246
+ if not app.exists:
247
+ print(f"Error: {app.dir_path} does not exist", file=sys.stderr)
248
+ sys.exit(1)
249
+
250
+ workflow = Workflow(kwargs['workflow_name'], app)
251
+ services = __get_services(workflow.services, filter=kwargs['filter'], service=kwargs['service'])
252
+
253
+ commands: List[WorkflowLogCommand] = []
254
+ for service in services:
255
+ if len(kwargs['service']) > 0 and service not in kwargs['service']:
256
+ continue
257
+
258
+ config = { **kwargs }
259
+ config['service_name'] = service
260
+ command = WorkflowLogCommand(app, **config)
261
+ commands.append(command)
262
+
263
+ commands = sorted(commands, key=lambda command: command.service_config.priority)
264
+
265
+ for command in commands:
266
+ __print_header(f"SERVICE {command.service_name}")
267
+
268
+ for shell_command in command.all():
269
+ print(shell_command)
270
+
271
+ if not kwargs['dry_run']:
272
+ exec_stream(shell_command)
273
+
274
+ @workflow.command()
275
+ @click.option('--app-dir-path', default=os.getcwd(), help='Path to application directory.')
276
+ @click.option('--certs-dir-path', default=DataDir.instance().certs_dir_path, help='Path to certs directory.')
277
+ @click.option('--context-dir-path', default=DataDir.instance().context_dir_path, help='Path to Stoobly data directory.')
278
+ @click.option('--filter', multiple=True, type=click.Choice([WORKFLOW_CUSTOM_FILTER]), help='Select which service groups to run. Defaults to all.')
279
+ @click.option('--dry-run', default=False, is_flag=True, help='If set, prints commands.')
280
+ @click.option('--extra-compose-path', help='Path to extra compose configuration files.')
281
+ @click.option('--log-level', default=INFO, type=click.Choice([DEBUG, INFO, WARNING, ERROR]), help='''
282
+ Log levels can be "debug", "info", "warning", or "error"
283
+ ''')
284
+ @click.option('--network', help='Name of network namespace.')
285
+ @click.option('--service', multiple=True, help='Select which services to run. Defaults to all.')
286
+ @click.argument('workflow_name')
287
+ def run(**kwargs):
288
+ cwd = os.getcwd()
289
+
290
+ if not os.getenv(env_vars.LOG_LEVEL):
291
+ os.environ[env_vars.LOG_LEVEL] = kwargs['log_level']
292
+
293
+ app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE, skip_validate_path=kwargs['dry_run'])
294
+ if kwargs['certs_dir_path']:
295
+ app.certs_dir_path = kwargs['certs_dir_path']
296
+
297
+ if kwargs['context_dir_path']:
298
+ app.context_dir_path = kwargs['context_dir_path']
299
+
300
+ if not app.exists:
301
+ print(f"Error: {app.dir_path} does not exist", file=sys.stderr)
302
+ sys.exit(1)
303
+
304
+ workflow = Workflow(kwargs['workflow_name'], app)
305
+ services = __get_services(workflow.services, filter=kwargs['filter'], service=kwargs['service'])
306
+
307
+ # Gateway ports are dynamically set depending on the workflow run
308
+ set_gateway_ports(workflow.service_paths_from_services(services))
309
+
310
+ commands: List[WorkflowRunCommand] = []
311
+ for service in services:
312
+ config = { **kwargs }
313
+ config['service_name'] = service
314
+ command = WorkflowRunCommand(app, **config)
315
+ command.current_working_dir = cwd
316
+ commands.append(command)
317
+
318
+ # Before services can be started, their network needs to be created
319
+ if len(commands) > 0:
320
+ create_network_command = commands[0].create_network()
321
+ if not kwargs['dry_run']:
322
+ exec_stream(create_network_command)
323
+ else:
324
+ print(create_network_command)
325
+
326
+ commands = sorted(commands, key=lambda command: command.service_config.priority)
327
+ for command in commands:
328
+ __print_header(f"SERVICE {command.service_name}")
329
+
330
+ exec_command = command.up()
331
+ if not exec_command:
332
+ continue
333
+
334
+ if not kwargs['dry_run']:
335
+ exec_stream(exec_command)
336
+ else:
337
+ print(exec_command)
338
+
339
+ scaffold.add_command(app)
340
+ scaffold.add_command(service)
341
+ scaffold.add_command(workflow)
342
+
343
+ def __get_services(services: List[str], **kwargs):
344
+ # Log services that don't exist
345
+ missing_services = [service for service in kwargs['service'] if service not in services]
346
+ if missing_services:
347
+ Logger.instance(WARNING).warn(f"Service(s) {','.join(missing_services)} are not found")
348
+
349
+ if kwargs['service']:
350
+ # If service is specified, run only those services
351
+ services = list(kwargs['service'])
352
+
353
+ if WORKFLOW_CUSTOM_FILTER not in kwargs['filter']:
354
+ services += CORE_SERVICES
355
+ else:
356
+ if WORKFLOW_CUSTOM_FILTER in kwargs['filter']:
357
+ # If this filter is set, then the user does not want to run core services
358
+ services = list(filter(lambda service: service not in CORE_SERVICES, services))
359
+
360
+ return list(set(services))
361
+
362
+ def __print_header(text: str):
363
+ Logger.instance(LOG_ID).info(f"{bcolors.OKBLUE}{text}{bcolors.ENDC}")
364
+
365
+ def __app_build(app, **kwargs):
366
+ AppCreateCommand(app, **kwargs).build()
367
+
368
+ def __scaffold_build(app, **kwargs):
369
+ command = ServiceCreateCommand(app, **kwargs)
370
+
371
+ command.build()
372
+
373
+ def __validate_app_dir(app_dir_path):
374
+ if not os.path.exists(app_dir_path):
375
+ print(f"Error: {app_dir_path} does not exist", file=sys.stderr)
376
+ sys.exit(1)
377
+
378
+ def __validate_service_dir(service_dir_path):
379
+ if not os.path.exists(service_dir_path):
380
+ print(f"Error: {service_dir_path} does not exist, please scaffold this service", file=sys.stderr)
381
+ sys.exit(1)
382
+
383
+ def __workflow_build(app, **kwargs):
384
+ command = WorkflowCreateCommand(app, **kwargs)
385
+
386
+ service_config = command.service_config
387
+ workflow_decorators = get_workflow_decorators(kwargs['template'], service_config)
388
+ command.build(
389
+ headless=kwargs['headless'],
390
+ template=kwargs['template'],
391
+ workflow_decorators=workflow_decorators
392
+ )
stoobly_agent/cli.py CHANGED
@@ -16,14 +16,14 @@ from stoobly_agent.config.data_dir import DataDir
16
16
  from stoobly_agent.lib.utils.conditional_decorator import ConditionalDecorator
17
17
 
18
18
  from .app.api import run as run_api
19
- from .app.cli import ca_cert, config, endpoint, feature, intercept, MainGroup, request, scenario, snapshot, trace
19
+ from .app.cli import ca_cert, config, endpoint, feature, intercept, MainGroup, request, scenario, scaffold, snapshot, trace
20
20
  from .app.cli.helpers.feature_flags import local, remote
21
21
  from .app.settings import Settings
22
22
  from .lib import logger
23
23
  from .lib.orm.migrate_service import migrate as migrate_database
24
24
  from .lib.utils.decode import decode
25
25
 
26
- settings = Settings.instance()
26
+ settings: Settings = Settings.instance()
27
27
  is_remote = remote(settings)
28
28
  is_local = local(settings)
29
29
 
@@ -50,6 +50,7 @@ main.add_command(endpoint)
50
50
  main.add_command(feature)
51
51
  main.add_command(intercept)
52
52
  main.add_command(request)
53
+ main.add_command(scaffold)
53
54
  main.add_command(scenario)
54
55
  main.add_command(snapshot)
55
56
  main.add_command(trace)
@@ -3,10 +3,11 @@ import shutil
3
3
 
4
4
  from stoobly_agent.config.constants.env_vars import ENV
5
5
 
6
+ DATA_DIR_NAME = '.stoobly'
7
+ DB_FILE_NAME = 'stoobly_agent.sqlite3'
8
+ DB_VERSION_NAME = 'VERSION'
9
+
6
10
  class DataDir:
7
- DATA_DIR_NAME = '.stoobly'
8
- DB_FILE_NAME = 'stoobly_agent.sqlite3'
9
- DB_VERSION_NAME = 'VERSION'
10
11
 
11
12
  _instances = None
12
13
 
@@ -15,10 +16,10 @@ class DataDir:
15
16
  raise RuntimeError('Call instance() instead')
16
17
  else:
17
18
  if path:
18
- self.__data_dir_path = os.path.join(path, self.DATA_DIR_NAME)
19
+ self.__data_dir_path = os.path.join(path, DATA_DIR_NAME)
19
20
  else:
20
21
  cwd = os.getcwd()
21
- self.__data_dir_path = os.path.join(cwd, self.DATA_DIR_NAME)
22
+ self.__data_dir_path = os.path.join(cwd, DATA_DIR_NAME)
22
23
 
23
24
  # If the current working directory does not contain a .stoobly folder,
24
25
  # then search in the parent directories until the home directory.
@@ -26,12 +27,12 @@ class DataDir:
26
27
  data_dir = self.find_data_dir(cwd)
27
28
 
28
29
  if not data_dir:
29
- self.__data_dir_path = os.path.join(os.path.expanduser('~'), self.DATA_DIR_NAME)
30
+ self.__data_dir_path = os.path.join(os.path.expanduser('~'), DATA_DIR_NAME)
30
31
  else:
31
32
  self.__data_dir_path = data_dir
32
33
 
33
34
  if not os.path.exists(self.__data_dir_path):
34
- os.makedirs(self.__data_dir_path, exist_ok=True)
35
+ self.create(os.path.dirname(self.__data_dir_path))
35
36
 
36
37
  @classmethod
37
38
  def instance(cls, path: str = None):
@@ -50,10 +51,14 @@ class DataDir:
50
51
 
51
52
  return cls.instance()
52
53
 
54
+ @property
55
+ def context_dir_path(self):
56
+ return os.path.abspath(os.path.join(self.path, '..'))
57
+
53
58
  @property
54
59
  def path(self):
55
60
  if os.environ.get(ENV) == 'test':
56
- test_path = os.path.join(self.__data_dir_path, 'tmp', self.DATA_DIR_NAME)
61
+ test_path = os.path.join(self.__data_dir_path, 'tmp', DATA_DIR_NAME)
57
62
 
58
63
  if not os.path.exists(test_path):
59
64
  os.makedirs(test_path, exist_ok=True)
@@ -71,6 +76,15 @@ class DataDir:
71
76
 
72
77
  return tmp_dir_path
73
78
 
79
+ @property
80
+ def certs_dir_path(self):
81
+ certs_dir_path = os.path.join(self.path, 'certs')
82
+
83
+ if not os.path.exists(certs_dir_path):
84
+ os.mkdir(certs_dir_path)
85
+
86
+ return certs_dir_path
87
+
74
88
  @property
75
89
  def db_dir_path(self):
76
90
  db_dir_path = os.path.join(self.path, 'db')
@@ -82,11 +96,11 @@ class DataDir:
82
96
 
83
97
  @property
84
98
  def db_file_path(self):
85
- return os.path.join(self.db_dir_path, self.DB_FILE_NAME)
99
+ return os.path.join(self.db_dir_path, DB_FILE_NAME)
86
100
 
87
101
  @property
88
102
  def db_version_path(self):
89
- return os.path.join(self.db_dir_path, self.DB_VERSION_NAME)
103
+ return os.path.join(self.db_dir_path, DB_VERSION_NAME)
90
104
 
91
105
  @property
92
106
  def settings_file_path(self):
@@ -152,22 +166,34 @@ class DataDir:
152
166
 
153
167
  def remove(self, directory_path = None):
154
168
  if directory_path:
155
- data_dir_path = os.path.join(directory_path, self.DATA_DIR_NAME)
169
+ data_dir_path = os.path.join(directory_path, DATA_DIR_NAME)
156
170
  else:
157
171
  data_dir_path = self.path
158
172
 
159
173
  if os.path.exists(data_dir_path):
160
- shutil.rmtree(data_dir_path)
174
+ shutil.rmtree(data_dir_path)
161
175
 
162
176
  def create(self, directory_path = None):
163
177
  if not directory_path:
164
178
  directory_path = os.getcwd()
165
179
 
166
- self.__data_dir_path = os.path.join(directory_path, self.DATA_DIR_NAME)
180
+ self.__data_dir_path = os.path.join(directory_path, DATA_DIR_NAME)
167
181
 
168
182
  if not os.path.exists(self.__data_dir_path):
169
183
  os.mkdir(self.__data_dir_path)
170
184
 
185
+ with open(os.path.join(self.__data_dir_path, '.gitignore'), 'w') as fp:
186
+ fp.write(
187
+ "\n".join([
188
+ 'db',
189
+ 'settings.yml',
190
+ os.path.join('snapshots', 'log'),
191
+ os.path.join('snapshots', 'VERSION'),
192
+ 'tmp'
193
+ ])
194
+ )
195
+
196
+
171
197
  def find_data_dir(self, start_path: str) -> str:
172
198
  # Note: these paths won't work for Windows
173
199
  root_dir = os.path.abspath(os.sep)
@@ -175,7 +201,7 @@ class DataDir:
175
201
  root_reached = False
176
202
 
177
203
  while start_path != home_dir:
178
- data_dir_path = os.path.join(start_path, self.DATA_DIR_NAME)
204
+ data_dir_path = os.path.join(start_path, DATA_DIR_NAME)
179
205
 
180
206
  if os.path.exists(data_dir_path):
181
207
  return data_dir_path
@@ -2,6 +2,7 @@ import logging
2
2
  import logging.config
3
3
  import os
4
4
  import pdb
5
+ import sys
5
6
 
6
7
  from typing import Literal
7
8
 
@@ -56,8 +57,8 @@ class Logger:
56
57
  logging.config.dictConfig({'disable_existing_loggers': True, 'version': 1})
57
58
  logging.basicConfig(level=logging.DEBUG)
58
59
  elif log_level.lower() == WARNING:
59
- logging.basicConfig(level=logging.WARNING)
60
+ logging.basicConfig(level=logging.WARNING, stream=sys.stderr)
60
61
  elif log_level.lower() == ERROR:
61
- logging.basicConfig(level=logging.ERROR)
62
+ logging.basicConfig(level=logging.ERROR, stream=sys.stderr)
62
63
  else:
63
64
  logging.basicConfig(level=logging.INFO)
@@ -1 +1 @@
1
- 0.34.11
1
+ 0.34.13
@@ -5,7 +5,7 @@ import pytest
5
5
 
6
6
  from stoobly_agent.config.constants.env_vars import ENV
7
7
  from stoobly_agent.config.constants.mode import NONE
8
- from stoobly_agent.config.data_dir import DataDir
8
+ from stoobly_agent.config.data_dir import DataDir, DATA_DIR_NAME
9
9
  from stoobly_agent.test.test_helper import reset
10
10
 
11
11
 
@@ -25,7 +25,7 @@ class TestDataDir():
25
25
  def test_in_home(self, original_cwd: str, home_dir: str):
26
26
  # A previous test can put us in 'stoobly_agent/test/app/models/schemas/.stoobly'
27
27
  os.chdir(original_cwd)
28
- data_dir_path = os.path.join(home_dir, DataDir.DATA_DIR_NAME)
28
+ data_dir_path = os.path.join(home_dir, DATA_DIR_NAME)
29
29
  os.environ[ENV] = NONE
30
30
 
31
31
  result = DataDir.instance().path
@@ -38,7 +38,7 @@ class TestDataDir():
38
38
 
39
39
  temp_dir = os.path.join(original_cwd, 'tmp')
40
40
  nested_temp_dir = os.path.join(temp_dir, 'tmp-nested')
41
- data_dir_path = os.path.join(nested_temp_dir, DataDir.DATA_DIR_NAME)
41
+ data_dir_path = os.path.join(nested_temp_dir, DATA_DIR_NAME)
42
42
  os.makedirs(data_dir_path)
43
43
 
44
44
  # Go into nested folder
@@ -68,7 +68,7 @@ class TestDataDir():
68
68
  os.makedirs(nested_dir3)
69
69
 
70
70
  # Create .stoobly folder in the nested structure
71
- data_dir_path = os.path.join(nested_dir1, DataDir.DATA_DIR_NAME)
71
+ data_dir_path = os.path.join(nested_dir1, DATA_DIR_NAME)
72
72
  os.makedirs(data_dir_path)
73
73
 
74
74
  # Go into dir3
@@ -1,27 +1,28 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: stoobly-agent
3
- Version: 0.34.13
3
+ Version: 1.0.1
4
4
  Summary: Record, mock, and test HTTP(s) requests. CLI agent for Stoobly
5
5
  License: Apache-2.0
6
6
  Author: Matt Le
7
7
  Author-email: themathewle@gmail.com
8
- Requires-Python: >=3.8,<4.0
8
+ Requires-Python: >=3.10,<4.0
9
9
  Classifier: License :: OSI Approved :: Apache Software License
10
10
  Classifier: Programming Language :: Python :: 3
11
- Classifier: Programming Language :: Python :: 3.8
12
- Classifier: Programming Language :: Python :: 3.9
13
11
  Classifier: Programming Language :: Python :: 3.10
14
12
  Classifier: Programming Language :: Python :: 3.11
15
13
  Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
16
15
  Requires-Dist: click (>=7.0.0,<8.0.0)
17
16
  Requires-Dist: diff-match-patch (>=20230430,<20230431)
18
17
  Requires-Dist: distro (>=1.6.0,<1.7.0)
18
+ Requires-Dist: dnspython (>=2.6.1,<2.7.0)
19
19
  Requires-Dist: httptools (>=0.4.0)
20
20
  Requires-Dist: jmespath (>=1.0.0)
21
21
  Requires-Dist: mergedeep (>=1.3.0,<1.3.4)
22
- Requires-Dist: mitmproxy (>=8.0.0,<=8.1.0)
22
+ Requires-Dist: mitmproxy (>=10.0.0,<11.0.0)
23
23
  Requires-Dist: multipart (>=0.2.5,<0.3.0)
24
- Requires-Dist: openapi-core (>=0.17.0,<0.18.0)
24
+ Requires-Dist: openapi-core (>=0.19.0,<0.20.0)
25
+ Requires-Dist: python-dotenv (>=1.0.1,<2.0.0)
25
26
  Requires-Dist: pyyaml (>=6.0.1)
26
27
  Requires-Dist: requests (>=2.31.0)
27
28
  Requires-Dist: stoobly_orator (>=0.9.12)
@@ -55,7 +56,7 @@ See our docs for more detailed information! https://docs.stoobly.com
55
56
 
56
57
  ## Prerequisite
57
58
 
58
- - Python >= 3.8, < 3.11
59
+ - Python 3.10, 3.11, 3.12
59
60
 
60
61
  ## Installation
61
62