22
33from typing import List , Tuple
44
5- import subprocess , os , sys
5+ import subprocess , os , sys , shutil , io
6+
7+ import json
8+
9+ _REPOSITORY_ROOT_PATH = os .path .abspath (os .path .join (os .path .dirname (os .path .abspath (__file__ )), '..' , '..' , '..' ))
10+
11+ """
12+ Intentions:
13+
14+ - Support markdown parsing.
15+ - Support sequential markdown code block execution, leveraging [info strings](https://spec.commonmark.org/0.30/#info-string).
16+ """
617
718class ASTNode (object ):
819
20+ _STACKQL_SHELL_INVOCATION : str = 'stackql-shell'
21+ _BASH : str = 'bash'
22+ _SETUP : str = 'setup'
23+ _TEARDOWN : str = 'teardown'
24+
925 def __init__ (self , node : dict ):
1026 self .node = node
1127 self .children = []
@@ -17,38 +33,44 @@ def get_type(self) -> str:
1733 return self .node .get ('type' , '' )
1834
1935 def get_text (self ) -> str :
20- return self .node .get ('text ' , '' )
36+ return self .node .get ('raw ' , '' ). strip ( )
2137
2238 def is_executable (self ) -> bool :
23- return self .get_type () == 'code_block'
39+ return self .get_type () == 'block_code'
40+
41+ def _get_annotations (self ) -> List [str ]:
42+ return self .node .get ('attrs' ).get ('info' , '' ).split (' ' )
43+
44+ def is_stackql_shell_invocation (self ) -> bool :
45+ return self ._STACKQL_SHELL_INVOCATION in self ._get_annotations ()
46+
47+ def is_bash (self ) -> bool :
48+ return self ._BASH in self ._get_annotations ()
49+
50+ def is_setup (self ) -> bool :
51+ return self ._SETUP in self ._get_annotations ()
52+
53+ def is_teardown (self ) -> bool :
54+ return self ._TEARDOWN in self ._get_annotations ()
2455
2556 def get_execution_language (self ) -> str :
2657 return self .node .get ('lang' , '' )
2758
59+ def __str__ (self ):
60+ return json .dumps (self .node , indent = 2 )
61+
62+ def __repr__ (self ):
63+ return self .__str__ ()
2864
2965class MdParser (object ):
3066
3167 def parse_markdown_file (self , file_path : str , lang = None ) -> List [ASTNode ]:
3268 markdown : mistune .Markdown = mistune .create_markdown (renderer = 'ast' )
3369 with open (file_path , 'r' ) as f :
3470 txt = f .read ()
35- return markdown (txt )
36-
37-
38- class MdExecutor (object ):
71+ raw_list : List [dict ] = markdown (txt )
72+ return [ASTNode (node ) for node in raw_list ]
3973
40- def __init__ (self , renderer : MdParser ):
41- self .renderer = renderer
42-
43- def execute (self , file_path : str ) -> None :
44- ast = self .renderer .parse_markdown_file (file_path )
45- for node in ast :
46- if node .is_executable ():
47- lang = node .get_execution_language ()
48- if lang == 'python' :
49- exec (node .get_text ())
50- else :
51- print (f'Unsupported language: { lang } ' )
5274
5375class WorkloadDTO (object ):
5476
@@ -65,20 +87,75 @@ def get_in_session(self) -> List[str]:
6587
6688 def get_teardown (self ) -> List [str ]:
6789 return self ._teardown
90+
91+ def __str__ (self ):
92+ return f'Setup: { self ._setup } \n In Session: { self ._in_session } \n Teardown: { self ._teardown } '
93+
94+ def __repr__ (self ):
95+ return self .__str__ ()
96+
97+ class MdOrchestrator (object ):
98+
99+ def __init__ (
100+ self ,
101+ parser : MdParser ,
102+ max_setup_blocks : int = 1 ,
103+ max_invocations_blocks : int = 1 ,
104+ max_teardown_blocks : int = 1 ,
105+ setup_contains_shell_invocation : bool = True
106+ ):
107+ self ._parser = parser
108+ self ._max_setup_blocks = max_setup_blocks
109+ self ._max_invocations_blocks = max_invocations_blocks
110+ self ._max_teardown_blocks = max_teardown_blocks
111+ self ._setup_contains_shell_invocation = setup_contains_shell_invocation
112+
113+ def orchestrate (self , file_path : str ) -> WorkloadDTO :
114+ setup_count : int = 0
115+ teardown_count : int = 0
116+ invocation_count : int = 0
117+ ast = self ._parser .parse_markdown_file (file_path )
118+ print (f'AST: { ast } ' )
119+ setup_str : str = f'cd { _REPOSITORY_ROOT_PATH } ;\n '
120+ in_session_commands : List [str ] = []
121+ teardown_str : str = f'cd { _REPOSITORY_ROOT_PATH } ;\n '
122+ for node in ast :
123+ if node .is_executable ():
124+ if node .is_setup ():
125+ if setup_count < self ._max_setup_blocks :
126+ setup_str += f'{ node .get_text ()} '
127+ setup_count += 1
128+ else :
129+ raise KeyError (f'Maximum setup blocks exceeded: { self ._max_setup_blocks } ' )
130+ elif node .is_teardown ():
131+ if teardown_count < self ._max_teardown_blocks :
132+ teardown_str += f'{ node .get_text ()} '
133+ teardown_count += 1
134+ else :
135+ raise KeyError (f'Maximum teardown blocks exceeded: { self ._max_teardown_blocks } ' )
136+ elif node .is_stackql_shell_invocation ():
137+ if invocation_count < self ._max_invocations_blocks :
138+ all_commands : str = node .get_text ().split ('\n \n ' )
139+ in_session_commands += all_commands
140+ invocation_count += 1
141+ else :
142+ raise KeyError (f'Maximum invocation blocks exceeded: { self ._max_invocations_blocks } ' )
143+ return WorkloadDTO (setup_str , in_session_commands , teardown_str )
68144
69145class SimpleE2E (object ):
70146
71147 def __init__ (self , workload : WorkloadDTO ):
72148 self ._workload = workload
73149
74150 def run (self ) -> Tuple [bytes , bytes ]:
151+ bash_path = shutil .which ('bash' )
75152 pr : subprocess .Popen = subprocess .Popen (
76153 self ._workload .get_setup (),
77154 stdin = subprocess .PIPE ,
78155 stdout = subprocess .PIPE ,
79156 stderr = subprocess .PIPE ,
80157 shell = True ,
81- executable = '/bin/bash'
158+ executable = bash_path
82159 )
83160 for cmd in self ._workload .get_in_session ():
84161 pr .stdin .write (f"{ cmd } \n " .encode (sys .getdefaultencoding ()))
@@ -87,16 +164,11 @@ def run(self) -> Tuple[bytes, bytes]:
87164 return (stdoout_bytes , stderr_bytes ,)
88165
89166if __name__ == '__main__' :
90- workload_dto : WorkloadDTO = WorkloadDTO (
91- setup = """
92- export GOOGLE_CREDENTIALS="$(cat /Users/admin/stackql/secrets/concerted-testing/google-credentials.json)" &&
93- stackql shell""" ,
94- in_session = [
95- 'registry pull google;' ,
96- 'select name, id FROM google.compute.instances WHERE project = \' ryuki-it-sandbox-01\' AND zone = \' australia-southeast1-a\' ;'
97- ],
98- teardown = 'echo "Goodbye, World!"'
99- )
167+ md_parser = MdParser ()
168+ orchestrator : MdOrchestrator = MdOrchestrator (md_parser )
169+ workload_dto : WorkloadDTO = orchestrator .orchestrate (os .path .join (_REPOSITORY_ROOT_PATH , 'docs' , 'walkthroughs' , 'get-google-vms.md' ))
170+ print (f'Workload DTO: { workload_dto } ' )
171+ # print(json.dumps(parsed_file, indent=2))
100172 e2e : SimpleE2E = SimpleE2E (workload_dto )
101173 stdout_bytes , stderr_bytes = e2e .run ()
102174 print (stdout_bytes .decode (sys .getdefaultencoding ()))
0 commit comments