Skip to content

Commit

Permalink
Add binder; Jupyter lab extensions; general cleanup. (#71)
Browse files Browse the repository at this point in the history
* restructure kernel to include errors and txt.

* update .gitignore

* bump dependencies in setup

* remove HTML output for logs

* remove pygments and fix logic error in log test.

* adjust log colors to better match

* bump version; remove pygments dependency

* remove pygemnts dependency.

* update regex to fix issues Tom found

* bump ver metakernel and package; push main branch

* move log caching to fix issue

* Update readme to be more current

* add binder info

* add binder svg

* clean up install and versioning

* modify postBuild to include Jlab extensions

* add sas_log_viewer to binder

* add server extension install to postBuild

* bump version; add jlab ext option; update readme

* http sascfg test

* set sascfg http time out to 30 seconds.

* bump version 2.4.1; handel connection errors.

* clean up pep issues

* update doc and binder use ['jlab_ext']

* clean up postBuild

* temp url while waiting for final location

* Delete testing

This file doesn't have value in the binder setup or SAS configuration
  • Loading branch information
jld23 authored Mar 19, 2021
1 parent 8ea86de commit 4434b5e
Show file tree
Hide file tree
Showing 10 changed files with 270 additions and 197 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ MANIFEST
**/*checkpoint.ipynb
**/*.ipynb
**/*.sas7bcat
sascfg_personal.py
**/*.ipynb_checkpoints/*
221 changes: 100 additions & 121 deletions Readme.md

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions binder/environment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
name: sas-compute-env
channels:
- conda-forge
dependencies:
- python
- pip
- jupyterlab=3
- pip:
- saspy
- sas_kernel['jlab_ext']
- pandas
34 changes: 34 additions & 0 deletions binder/postBuild
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/bin/bash

#
# Copyright SAS Institute
#
# Licensed under the Apache License, Version 2.0 (the License);
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

# copy sascfg_personal.py to default location
mkdir -p ~/.config/saspy/
cp ./binder/sascfg_personal.py ~/.config/saspy/

#set -ex
jupyter kernelspec list
jupyter nbextension list


jupyter nbextension install --py sas_kernel.showSASLog --user
jupyter nbextension enable sas_kernel.showSASLog --py

jupyter nbextension install --py sas_kernel.theme --user
jupyter nbextension enable sas_kernel.theme --py

jupyter nbextension list
8 changes: 8 additions & 0 deletions binder/sascfg_personal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
SAS_config_names = ['viya4']

viya4 = {
'url':'https://temp.url.com:443',
'verify': False,
'timeout': 30,
'context':'SAS Job Execution compute context'
}
1 change: 0 additions & 1 deletion sas_kernel/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,3 @@


"""SAS Kernel Juypter Implementation"""
from sas_kernel.version import __version__
3 changes: 1 addition & 2 deletions sas_kernel/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,4 @@

from ipykernel.kernelapp import IPKernelApp
from .kernel import SASKernel

IPKernelApp.launch_instance(kernel_class=SASKernel)
IPKernelApp.launch_instance(kernel_class=SASKernel)
2 changes: 1 addition & 1 deletion sas_kernel/version.py → sas_kernel/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@
# limitations under the License.
#

__version__ = '2.4.1'

__version__ = '2.3.1'
162 changes: 97 additions & 65 deletions sas_kernel/kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,32 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
import base64
import os
import sys
import re
import json
# Create Logger
import types
import importlib.machinery
# Create LOGGER
import logging
import saspy

from typing import Tuple

from metakernel import MetaKernel
from sas_kernel.version import __version__
from IPython.display import HTML
from metakernel import MetaKernel
from ._version import __version__


# create a LOGGER to output messages to the Jupyter CONSOLE
LOGGER = logging.getLogger(__name__)
LOGGER.setLevel(logging.WARN)
CONSOLE = logging.StreamHandler()
CONSOLE.setFormatter(logging.Formatter('%(name)-12s: %(message)s'))
LOGGER.addHandler(CONSOLE)

LOGGER.debug("sanity check")

# create a logger to output messages to the Jupyter console
logger = logging.getLogger(__name__)
logger.setLevel(logging.WARN)
console = logging.StreamHandler()
console.setFormatter(logging.Formatter('%(name)-12s: %(message)s'))
logger.addHandler(console)

logger.debug("sanity check")
class SASKernel(MetaKernel):
"""
SAS Kernel for Jupyter implementation. This module relies on SASPy
Expand All @@ -50,17 +55,19 @@ class SASKernel(MetaKernel):
}

def __init__(self, **kwargs):
with open(os.path.dirname(os.path.realpath(__file__)) + '/data/' + 'sasproclist.json') as proclist:
with open(os.path.dirname(os.path.realpath(__file__)) + \
'/data/' + 'sasproclist.json') as proclist:
self.proclist = json.load(proclist)
with open(os.path.dirname(os.path.realpath(__file__)) + '/data/' + 'sasgrammardictionary.json') as compglo:
with open(os.path.dirname(os.path.realpath(__file__)) + \
'/data/' + 'sasgrammardictionary.json') as compglo:
self.compglo = json.load(compglo)
self.strproclist = '\n'.join(str(x) for x in self.proclist)
self.promptDict = {}
MetaKernel.__init__(self, **kwargs)
self.mva = None
self.cachedlog = None
self.lst_len = -99 # initialize the length to a negative number to trigger function
# print(dir(self))
self._allow_stdin = False

def do_apply(self, content, bufs, msg_id, reply_metadata):
pass
Expand All @@ -71,6 +78,15 @@ def do_clear(self):
def get_usage(self):
return "This is the SAS kernel."

def _get_config_names(self):
"""
get the config file used by SASPy
"""
loader = importlib.machinery.SourceFileLoader('foo', saspy.SAScfg)
cfg = types.ModuleType(loader.name)
loader.exec_module(cfg)
return cfg.SAS_config_names

def _get_lst_len(self):
code = "data _null_; run;"
res = self.mva.submit(code)
Expand All @@ -81,11 +97,25 @@ def _get_lst_len(self):

def _start_sas(self):
try:
import saspy as saspy
# import saspy as saspy
self.mva = saspy.SASsession(kernel=self)
except:
except KeyError:
self.mva = None
except OSError:#socket.gaierror
msg = """Failed to connect to SAS!
Please check your connection configuration here:{0}
Here are the valid configurations:{1}
You can load the configuration file into a Jupyter Lab cell using this command:
%load {0}
If the URL/Path are correct the issue is likely your username and/or password
""".format(saspy.list_configs()[0], ', '.join(self._get_config_names()))
self.Error_display(msg)
self.mva = None

except:
print("Unexpected error:", sys.exc_info()[0])
raise


def _colorize_log(self, log: str) -> str:
"""
takes a SAS log (str) and then looks for errors.
Expand All @@ -109,7 +139,6 @@ def _colorize_log(self, log: str) -> str:

return colored_log


def _is_error_log(self, log: str) -> Tuple:
"""
takes a SAS log (str) and then looks for errors.
Expand All @@ -120,49 +149,54 @@ def _is_error_log(self, log: str) -> Tuple:
error_log_msg_list = []
error_log_line_list = []
for index, line in enumerate(lines):
#logger.debug("line:{}".format(line))
# LOGGER.debug("line:{}".format(line))
if line.startswith('ERROR'):
error_count +=1
error_log_msg_list.append(line)
error_log_line_list.append(index)
return (error_count, error_log_msg_list, error_log_line_list)

error_count += 1
error_log_msg_list.append(line)
error_log_line_list.append(index)
return (error_count, error_log_msg_list, error_log_line_list)

def _which_display(self, log: str, output: str = '') -> str:
"""
Determines if the log or lst should be returned as the results for the cell based on parsing the log
Determines if the log or lst should be returned as the
results for the cell based on parsing the log
looking for errors and the presence of lst output.
:param log: str log from code submission
:param output: None or str lst output if there was any
:return: The correct results based on log and lst
:rtype: str
"""
error_count, msg_list, error_line_list = self._is_error_log(log)
error_count, msg_list, error_line_list = self._is_error_log(log)

# store the log for display in the showSASLog nbextension
self.cachedlog = self._colorize_log(log)

if error_count == 0 and len(output) > self.lst_len: # no error and LST output
#self.cachedlog = self._colorize_log(log)

# no error and LST output
if error_count == 0 and len(output) > self.lst_len:
return self.Display(HTML(output))

elif error_count > 0 and len(output) > self.lst_len: # errors and LST
#filter log to lines around first error
# filter log to lines around first error
# by default get 5 lines on each side of the first Error message.
# to change that modify the values in {} below
regex_around_error = r"(.*)(.*\n){6}^ERROR(.*\n){6}"

# Extract the first match +/- 5 lines
e_log = re.search(regex_around_error, log, re.MULTILINE).group()
assert error_count == len(error_line_list), "Error count and count of line number don't match"
return self.Error_display(msg_list[0], print(self._colorize_log(e_log)), HTML(output))

assert error_count == len(
error_line_list), "Error count and count of line number don't match"
return self.Error_display(msg_list[0],
print(self._colorize_log(e_log)),
HTML(output))

# for everything else return the log
return self.Print(self._colorize_log(log))

def do_execute_direct(self, code: str, silent: bool = False) -> [str, dict]:
"""
This is the main method that takes code from the Jupyter cell and submits it to the SAS server
This is the main method that takes code from the Jupyter cell
and submits it to the SAS server.
:param code: code from the cell
:param silent:
Expand All @@ -182,10 +216,8 @@ def do_execute_direct(self, code: str, silent: bool = False) -> [str, dict]:
self._get_lst_len()

# This block uses special strings submitted by the Jupyter notebook extensions
if code.startswith('showSASLog_11092015') == False and code.startswith("CompleteshowSASLog_11092015") == False:
logger.debug("code type: " + str(type(code)))
logger.debug("code length: " + str(len(code)))
logger.debug("code string: " + code)
if not code.startswith('showSASLog_11092015') and \
not code.startswith("CompleteshowSASLog_11092015"):
if code.startswith("/*SASKernelTest*/"):
res = self.mva.submit(code, "text")
else:
Expand All @@ -195,20 +227,23 @@ def do_execute_direct(self, code: str, silent: bool = False) -> [str, dict]:
print(res['LOG'], '\n' "Restarting SAS session on your behalf")
self.do_shutdown(True)
return res['LOG']


# store the log for display in the showSASLog nbextension
self.cachedlog = self._colorize_log(res['LOG'])

# Parse the log to check for errors
error_count, error_log_msg, _ = self._is_error_log(res['LOG'])

if error_count > 0 and len(res['LST']) <= self.lst_len:
return(self.Error(error_log_msg[0], print(self._colorize_log(res['LOG']))))
return self.Error(error_log_msg[0], print(self._colorize_log(res['LOG'])))

return self._which_display(res['LOG'], res['LST'])

elif code.startswith("CompleteshowSASLog_11092015") == True and code.startswith('showSASLog_11092015') == False:
return (self.Print(self._colorize_log(self.mva.saslog())))
elif code.startswith("CompleteshowSASLog_11092015") and \
not code.startswith('showSASLog_11092015'):
return self.Print(self._colorize_log(self.mva.saslog()))
else:
return (self.Print(self._colorize_log(self.cachedlog)))

return self.Print(self._colorize_log(self.cachedlog))

def get_completions(self, info):
"""
Expand All @@ -220,7 +255,8 @@ def get_completions(self, info):
relstart = info['start']
seg = info['line'][:relstart]
if relstart > 0 and re.match('(?i)proc', seg.rsplit(None, 1)[-1]):
potentials = re.findall('(?i)^' + info['obj'] + '.*', self.strproclist, re.MULTILINE)
potentials = re.findall(
'(?i)^' + info['obj'] + '.*', self.strproclist, re.MULTILINE)
return potentials
else:
lastproc = info['code'].lower()[:info['help_pos']].rfind('proc')
Expand All @@ -241,10 +277,11 @@ def get_completions(self, info):
mykey = 's'
if lastproc > lastsemi:
mykey = 'p'
procer = re.search('(?i)proc\s\w+', info['code'][lastproc:])
procer = re.search(r'(?i)proc\s\w+', info['code'][lastproc:])
method = procer.group(0).split(' ')[-1].upper() + mykey
mylist = self.compglo[method][0]
potentials = re.findall('(?i)' + info['obj'] + '.+', '\n'.join(str(x) for x in mylist), re.MULTILINE)
potentials = re.findall(
'(?i)' + info['obj'] + '.+', '\n'.join(str(x) for x in mylist), re.MULTILINE)
return potentials
elif data:
# we are in statements (probably if there is no data)
Expand All @@ -255,30 +292,31 @@ def get_completions(self, info):
if lastproc > lastsemi:
mykey = 'p'
mylist = self.compglo['DATA' + mykey][0]
potentials = re.findall('(?i)^' + info['obj'] + '.*', '\n'.join(str(x) for x in mylist), re.MULTILINE)
potentials = re.findall(
'(?i)^' + info['obj'] + '.*', '\n'.join(str(x) for x in mylist), re.MULTILINE)
return potentials
else:
potentials = ['']
return potentials

@staticmethod
def _get_right_list(s):
proc_opt = re.search(r"proc\s(\w+).*?[^;]\Z", s, re.IGNORECASE | re.MULTILINE)
proc_stmt = re.search(r"\s*proc\s*(\w+).*;.*\Z", s, re.IGNORECASE | re.MULTILINE)
data_opt = re.search(r"\s*data\s*[^=].*[^;]?.*$", s, re.IGNORECASE | re.MULTILINE)
data_stmt = re.search(r"\s*data\s*[^=].*[^;]?.*$", s, re.IGNORECASE | re.MULTILINE)
proc_opt = re.search(
r"proc\s(\w+).*?[^;]\Z", s, re.IGNORECASE | re.MULTILINE)
proc_stmt = re.search(r"\s*proc\s*(\w+).*;.*\Z",
s, re.IGNORECASE | re.MULTILINE)
data_opt = re.search(
r"\s*data\s*[^=].*[^;]?.*$", s, re.IGNORECASE | re.MULTILINE)
data_stmt = re.search(
r"\s*data\s*[^=].*[^;]?.*$", s, re.IGNORECASE | re.MULTILINE)
print(s)
if proc_opt:
logger.debug(proc_opt.group(1).upper() + 'p')
return proc_opt.group(1).upper() + 'p'
elif proc_stmt:
logger.debug(proc_stmt.group(1).upper() + 's')
return proc_stmt.group(1).upper() + 's'
elif data_opt:
logger.debug("data step")
return 'DATA' + 'p'
elif data_stmt:
logger.debug("data step")
return 'DATA' + 's'
else:
return None
Expand Down Expand Up @@ -306,9 +344,3 @@ def do_shutdown(self, restart):
self.restart_kernel()
self.Print("Done!")
return {'status': 'ok', 'restart': restart}


if __name__ == '__main__':
from ipykernel.kernelapp import IPKernelApp
from .kernel import SASKernel
IPKernelApp.launch_instance(kernel_class=SASKernel)
Loading

0 comments on commit 4434b5e

Please sign in to comment.