20
20
# Create Logger
21
21
import logging
22
22
23
+ from typing import Tuple
24
+
23
25
from metakernel import MetaKernel
24
- from sas_kernel import __version__
26
+ from sas_kernel . version import __version__
25
27
from IPython .display import HTML
26
- # color syntax for the SASLog
27
- from saspy .SASLogLexer import SASLogStyle , SASLogLexer
28
- from pygments .formatters import HtmlFormatter
29
- from pygments import highlight
30
28
31
- logger = logging .getLogger ('' )
29
+ # create a logger to output messages to the Jupyter console
30
+ logger = logging .getLogger (__name__ )
32
31
logger .setLevel (logging .WARN )
32
+ console = logging .StreamHandler ()
33
+ console .setFormatter (logging .Formatter ('%(name)-12s: %(message)s' ))
34
+ logger .addHandler (console )
33
35
36
+ logger .debug ("sanity check" )
34
37
class SASKernel (MetaKernel ):
35
38
"""
36
39
SAS Kernel for Jupyter implementation. This module relies on SASPy
@@ -82,8 +85,50 @@ def _start_sas(self):
82
85
self .mva = saspy .SASsession (kernel = self )
83
86
except :
84
87
self .mva = None
88
+
89
+ def _colorize_log (self , log : str ) -> str :
90
+ """
91
+ takes a SAS log (str) and then looks for errors.
92
+ Returns a tuple of error count, list of error messages
93
+ """
94
+ regex_note = r"(?m)(^NOTE.*((\n|\t|\n\t)[ ]([^WEN].*)(.*))*)"
95
+ regex_warn = r"(?m)(^WARNING.*((\n|\t|\n\t)[ ]([^WEN].*)(.*))*)"
96
+ regex_error = r"(?m)(^ERROR.*((\n|\t|\n\t)[ ]([^WEN].*)(.*))*)"
97
+
98
+ sub_note = "\x1b [38;5;21m\\ 1\x1b [0m"
99
+ sub_warn = "\x1b [38;5;2m\\ 1\x1b [0m"
100
+ sub_error = "\x1B [1m\x1b [38;5;9m\\ 1\x1b [0m\x1b [0m"
101
+ color_pattern = [
102
+ (regex_error , sub_error ),
103
+ (regex_note , sub_note ),
104
+ (regex_warn , sub_warn )
105
+ ]
106
+ colored_log = log
107
+ for pat , sub in color_pattern :
108
+ colored_log = re .sub (pat , sub , colored_log )
109
+
110
+ return colored_log
111
+
112
+
113
+ def _is_error_log (self , log : str ) -> Tuple :
114
+ """
115
+ takes a SAS log (str) and then looks for errors.
116
+ Returns a tuple of error count, list of error messages
117
+ """
118
+ lines = re .split (r'[\n]\s*' , log )
119
+ error_count = 0
120
+ error_log_msg_list = []
121
+ error_log_line_list = []
122
+ for index , line in enumerate (lines ):
123
+ #logger.debug("line:{}".format(line))
124
+ if line .startswith ('ERROR' ):
125
+ error_count += 1
126
+ error_log_msg_list .append (line )
127
+ error_log_line_list .append (index )
128
+ return (error_count , error_log_msg_list , error_log_line_list )
129
+
85
130
86
- def _which_display (self , log : str , output : str ) -> HTML :
131
+ def _which_display (self , log : str , output : str = '' ) -> str :
87
132
"""
88
133
Determines if the log or lst should be returned as the results for the cell based on parsing the log
89
134
looking for errors and the presence of lst output.
@@ -93,40 +138,27 @@ def _which_display(self, log: str, output: str) -> HTML:
93
138
:return: The correct results based on log and lst
94
139
:rtype: str
95
140
"""
96
- lines = re .split (r'[\n]\s*' , log )
97
- i = 0
98
- elog = []
99
- for line in lines :
100
- i += 1
101
- e = []
102
- if line .startswith ('ERROR' ):
103
- logger .debug ("In ERROR Condition" )
104
- e = lines [(max (i - 15 , 0 )):(min (i + 16 , len (lines )))]
105
- elog = elog + e
106
- tlog = '\n ' .join (elog )
107
- logger .debug ("elog count: " + str (len (elog )))
108
- logger .debug ("tlog: " + str (tlog ))
109
-
110
- color_log = highlight (log , SASLogLexer (), HtmlFormatter (full = True , style = SASLogStyle , lineseparator = "<br>" ))
141
+ error_count , msg_list , error_line_list = self ._is_error_log (log )
142
+
111
143
# store the log for display in the showSASLog nbextension
112
- self .cachedlog = color_log
113
- # Are there errors in the log? if show the lines on each side of the error
114
- if len ( elog ) == 0 and len (output ) > self .lst_len : # no error and LST output
115
- debug1 = 1
116
- logger . debug ( "DEBUG1: " + str ( debug1 ) + " no error and LST output " )
117
- return HTML (output )
118
- elif len ( elog ) == 0 and len ( output ) <= self . lst_len : # no error and no LST
119
- debug1 = 2
120
- logger . debug ( "DEBUG1: " + str ( debug1 ) + " no error and no LST" )
121
- return HTML ( color_log )
122
- elif len ( elog ) > 0 and len ( output ) <= self . lst_len : # error and no LST
123
- debug1 = 3
124
- logger . debug ( "DEBUG1: " + str ( debug1 ) + " error and no LST" )
125
- return HTML ( color_log )
126
- else : # errors and LST
127
- debug1 = 4
128
- logger . debug ( "DEBUG1: " + str ( debug1 ) + " errors and LST" )
129
- return HTML ( color_log + output )
144
+ self .cachedlog = self . _colorize_log ( log )
145
+
146
+ if error_count == 0 and len (output ) > self .lst_len : # no error and LST output
147
+ return self . Display ( HTML ( output ))
148
+
149
+ elif error_count > 0 and len (output ) > self . lst_len : # errors and LST
150
+ #filter log to lines around first error
151
+ # by default get 5 lines on each side of the first Error message.
152
+ # to change that modify the values in {} below
153
+ regex_around_error = r"(.*)(.*\n){6}^ERROR(.*\n){6}"
154
+
155
+ # Extract the first match +/- 5 lines
156
+ e_log = re . search ( regex_around_error , log , re . MULTILINE ). group ( )
157
+ assert error_count == len ( error_line_list ), "Error count and count of line number don't match"
158
+ return self . Error_display ( msg_list [ 0 ], print ( self . _colorize_log ( e_log )), HTML ( output ))
159
+
160
+ # for everything else return the log
161
+ return self . Print ( self . _colorize_log ( log ) )
130
162
131
163
def do_execute_direct (self , code : str , silent : bool = False ) -> [str , dict ]:
132
164
"""
@@ -140,19 +172,16 @@ def do_execute_direct(self, code: str, silent: bool = False) -> [str, dict]:
140
172
return {'status' : 'ok' , 'execution_count' : self .execution_count ,
141
173
'payload' : [], 'user_expressions' : {}}
142
174
175
+ # If no mva session start a session
143
176
if self .mva is None :
144
177
self ._allow_stdin = True
145
178
self ._start_sas ()
146
179
180
+ # This code is now handeled in saspy will remove in future version
147
181
if self .lst_len < 0 :
148
182
self ._get_lst_len ()
149
183
150
- if code .startswith ('Obfuscated SAS Code' ):
151
- logger .debug ("decoding string" )
152
- tmp1 = code .split ()
153
- decode = base64 .b64decode (tmp1 [- 1 ])
154
- code = decode .decode ('utf-8' )
155
-
184
+ # This block uses special strings submitted by the Jupyter notebook extensions
156
185
if code .startswith ('showSASLog_11092015' ) == False and code .startswith ("CompleteshowSASLog_11092015" ) == False :
157
186
logger .debug ("code type: " + str (type (code )))
158
187
logger .debug ("code length: " + str (len (code )))
@@ -166,17 +195,20 @@ def do_execute_direct(self, code: str, silent: bool = False) -> [str, dict]:
166
195
print (res ['LOG' ], '\n ' "Restarting SAS session on your behalf" )
167
196
self .do_shutdown (True )
168
197
return res ['LOG' ]
198
+
199
+ # Parse the log to check for errors
200
+ error_count , error_log_msg , _ = self ._is_error_log (res ['LOG' ])
201
+
202
+ if error_count > 0 and len (res ['LST' ]) <= self .lst_len :
203
+ return (self .Error (error_log_msg [0 ], print (self ._colorize_log (res ['LOG' ]))))
204
+
205
+ return self ._which_display (res ['LOG' ], res ['LST' ])
169
206
170
- output = res ['LST' ]
171
- log = res ['LOG' ]
172
- return self ._which_display (log , output )
173
207
elif code .startswith ("CompleteshowSASLog_11092015" ) == True and code .startswith ('showSASLog_11092015' ) == False :
174
- full_log = highlight (self .mva .saslog (), SASLogLexer (),
175
- HtmlFormatter (full = True , style = SASLogStyle , lineseparator = "<br>" ,
176
- title = "Full SAS Log" ))
177
- return full_log .replace ('\n ' , ' ' )
208
+ return (self .Print (self ._colorize_log (self .mva .saslog ())))
178
209
else :
179
- return self .cachedlog .replace ('\n ' , ' ' )
210
+ return (self .Print (self ._colorize_log (self .cachedlog )))
211
+
180
212
181
213
def get_completions (self , info ):
182
214
"""
@@ -279,5 +311,4 @@ def do_shutdown(self, restart):
279
311
if __name__ == '__main__' :
280
312
from ipykernel .kernelapp import IPKernelApp
281
313
from .kernel import SASKernel
282
- from sas_kernel import __version__
283
314
IPKernelApp .launch_instance (kernel_class = SASKernel )
0 commit comments