Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SAS IOM Interface Using Windows COM #228

Merged
merged 19 commits into from May 16, 2019

Conversation

hhubbell
Copy link
Contributor

@hhubbell hhubbell commented Apr 29, 2019

Hi Tom,

Users at our org are provisioned an EG installation on their client machine to interact with the SAS server. The install seems a bit unique in that no Java code is installed anywhere on the client - in fact, I don't think any of the clients have a Java runtime installed at all. This has been a major roadblock to using this library, because we are all on Windows machines (no STDIO or SSH IO methods). We have some enthusiastic analysts that are interested in using Python in conjunction with pre-existing datasets/macros/etc.

In my spare time over the past few months I've put together an IO module that uses the SAS COM libraries, which gets installed with EG, to create a bridge between the client and server. I'd like to contribute this module back upstream in hopes that it may help some other users that do not want to use Java as the bridge. Perhaps a band-aid for #206?

Overview

Creating the bridge is actually fairly straightforward. The implementation details can be found in Chris Hemedinger's blog post Using Windows PowerShell to connect to a SAS Workspace server. There's a little bit of digging required in the MSDN and SAS LanguageService docs for some stuff, but otherwise nothing too fancy I think.

Known Issues

There is one known issue related to client/server file IO that I hope is not a deal breaker. I think this is something that we can fix in the future, but I'd like a second pair of eyes on it. I don't think it's major, as I experience the same issue in SAS EG anyway. This leads me to believe that the behavior is related to a configuration issue, or that it is expected behavior.

Some IO methods such as read_csv and write_csv that take a file path as a parameter may read or write from the server's file path instead of the client's. Here's an example:

my_file = "C:/Path/To/MyFile.csv"    # File exists on my computer
sas.read_csv(my_file, "my_file")     # SAS thinks I'm referencing a file on the server

If that file exists on the SAS server, that file will be read instead. If it does not exists, an error is returned. Like I mentioned, I can reproduce this using SAS EG, so it may be a config issue at my org or expected behavior. If I reference a file on a shared drive, it works fine (both EG and this IO module read/write without issues).

Tests

This module passes 109/113 tests. Five tests are skipped; three are errors that exist regardless of IO method used (they are attempting to skip the tests anyway), and two tests are skipped outright. I introduced a few checks that validated the proper methods were defined. This was a pain point during development as some "underscore" methods are used publicly. They aren't really "unit tests," but they just check to see if the API is consistent.

----------------------------------------------------------------------
Ran 118 tests in 457.788s

FAILED (failures=4, errors=3, skipped=2)

The four tests that fail are due to the known issue described above. Both test_read_csv and test_write_csv fail due to the file path, as well as regScoreAssess and regScoreAssess2 which attempt to write to a temporary file. These show up with unrelated errors during testing as a result.

======================================================================
FAIL: test_regScoreAssess (test_sasdata.TestSASdataObject)
----------------------------------------------------------------------
AssertionError: False is not true : Prediction Column not found

======================================================================
FAIL: test_regScoreAssess2 (test_sasdata.TestSASdataObject)
----------------------------------------------------------------------
AssertionError: False is not true : Prediction Column not found

======================================================================
FAIL: test_SASsession_csv_read (test_sassession.TestSASsessionObject)
----------------------------------------------------------------------
AssertionError: csvcars.head() result didn't contain row 1

======================================================================
FAIL: test_sassession_csv_write (test_sassession.TestSASsessionObject)
----------------------------------------------------------------------
AssertionError: 'ERROR' unexpectedly found in "[REDACTED FOR BREVITY]"

Please let me know if you have any questions or concerns.

Thanks!

hhubbell and others added 4 commits April 29, 2019 17:59
Added tests to validate SAS IO related functionality. Currently, the
only tests written are to check for IO object compliance. That is, the
tests make sure that each IO object has the required methods defined so
that all objects expose a coherent interface. This was particularly
tricky when defining the COM module as some private functions need to be
exposed publicly (`_asubmit`).

Additional tests would be useful.
1. Resolve issue where any invalid SAS syntax put the LanguageService in
   to an error state. For instance, the following would return an ERROR
   in the log, but also cause strange behavior upon issuance of
   additional commands: `data foo; set bar run;`. The missing semicolon
   after `set bar` would cause LanguageService token scanning to totally
   explode. By issuing a reset command, the scanner no longer enters
   this state.
2. Missing `nosub` arguments in `read_csv` and `write_csv` were causing
   some tests to fail. Add support for this argument as well.
3. Resolve issue where writing a dataframe with datetime values was
   setting the improper date format (DATETIME20. instead of
   E8601DT26.6). After some testing, it appears that ADODB can support
   writing this format, and the original issue was likely due to the
   tired programmer.
@tomweber-sas
Copy link
Contributor

Hey @hhubbell that's pretty cool! I haven't had a chance to dig through it all or try it, but at a glance, it sounds cool.

For the read/write_csv methods, those are server side, so that's no issue. They are just proc import/|export, so the filesystem path has to be accessible to the server. I recently added upload/download methods so that you can upload a client side file and then access it on the server, or you can do write_csv() followed by download() to write it out w/ SAS and then pull it to your client; - same for the other direction. The up/down is a binary transfer, so any kind of file will work. You can transfer SAS Data Sets if you want.

A couple things right off the top of my head are support for this, both w/ issues and with enhancements.
Are you looking to support it going forward? This is a new area I haven't hit as of yet regarding this level of contribution. What are you thinking on this front?

Also, dependencies; saspy isn't dependent on anything that's not in the standard library, and it can't be dependent on windows or linux specific things, as it can be installed and run on the various platforms with only 1 of any of the access methods. It's not even dependent on pandas, as you don't have to have that to still be able to use 90% of the functionality. So, this would be an optional access method to use (they all are), and saspy should install and run w/out any of it's dependencies, unless, of course, you are trying to use it, then you obviously need them installed for it to work. I don't see any issue with this, They would be runtime dependencies if you try to run it this way, but not be required to install on any platfor if using other access methods.

The other thing just skimming over it is the host/port and user/pw are the same as the current IOM access method, since they are IOM, just a different client library, that only works on windows. So, I think those config parameters should be the same as the java IOM client access method; iomhost/iomport omruser/omrpw.

So, I'll try to look into this further and see what else I see. Seems like a cool thing.

Thanks,
Tom

@hhubbell
Copy link
Contributor Author

Hi Tom,

Thanks for your feedback.

For the read/write_csv methods, those are server side, so that's no issue. They are just proc import/|export, so the filesystem path has to be accessible to the server.

Great. That was my understanding as well, but I wasn't sure if it worked differently with the Java IOM client.

Are you looking to support it going forward? This is a new area I haven't hit as of yet regarding this level of contribution. What are you thinking on this front?

I plan to provide support as needed going forward. I enjoy working on this project because I like Python and I think the library is useful for my professional work, but this work is a side-project for me. I'll do my best to contribute in a timely manner if there's a critical issue, but it will always come third to my paying job and my sanity :)

I think this is a great library and I hope to continue working on it.

Also, dependencies; saspy isn't dependent on anything that's not in the standard library, and it can't be dependent on windows or linux specific things, as it can be installed and run on the various platforms with only 1 of any of the access methods.

Right, the IOMCOM access method does require an additional Python library to interact with the COM API. The library is pywin32. I've maintained compatibility with Linux platforms in the following ways:

In setup.py, I've added an exrtas_require argument. This tells the install script to only install the pywin32 library if the target is a Windows platform. It's also possible to not include this in setup.py at all and simply document that pywin32 needs to be installed for the IOM COM access method. Up to you.

extras_require = {':platform_system == "Windows"': ['pypiwin32']}

In sasiocom.py, I import pywin32 in an if statement. If we choose to remove the extras_require section from setup.py, this will just be changed to a try-catch block.

if platform.system() == 'Windows':
    from win32com.client import dynamic

I think those config parameters should be the same as the java IOM client access method; iomhost/iomport omruser/omrpw.

Noted. I will update.

@FriedEgg
Copy link
Contributor

FriedEgg commented May 1, 2019

I don't mind helping with support either, I have been using the COM api in python for years, just never had the time to contribute like @hhubbell has (well done!). I think that it may be the right time to consider moving the different connection types/io types into their own plugins to SASPy. It would cleanup the dependencies problem, at the very least.

@tomweber-sas
Copy link
Contributor

@FriedEgg thanks for offering to help out with this, greatly appreciated.
@hhubbell I'm looking through this more now, and it's looking good; next is to install it and play around and see what I see. changing the host/port and user/pw to be the same as the existing IOM access method is good, and now with the explanation of extras_requirement (#227), I see that the pypiwin32 won't be a saspy dependency, so that issue is also resolved. That was the only non-standard library module being imported by this, right? Seemed like it.
I see that the upload/download methods aren't in the access method module. Were you working on adding those? I think they are the only things missing from that layer of code (I'll see when I play around with it).
I'll install it next and work it through my usual progressions.

Thanks!
Tom

@tomweber-sas
Copy link
Contributor

Well I got the following error doing the install. And, the more specific question I have is why it appeared to try to install pypiwin32, presumably from the etras_requitements??? It references saspy as why it was trying to install it (if I read this right). I thought it wasn't supposed to install that unless I added it to the install line like
pip install saspy['pypiwin32']
or something like that which you showed in the other thread.

And, since it couldn't install that, for other reasons (I don't really care the specific reason), saspy install failed. That's what I don't want, as I didn't ask for the extra requirement. If I was only going to use the existing IOM module, I don't need it trying to install win32 and now I can't install saspy. That's one of the issues I have with adding extra optional requirements in a way that isn't optional.

Does the extras_requirement in this version of your setup.py just need to be coded different than it is now, and then it won't try to install that unless I ask for it? That's what I thought you were describing. If it can work that way, then that's cool.

Thanks!
Tom

Collecting pypiwin32 (from saspy==2.4.4)
  Downloading https://files.pythonhosted.org/packages/d0/1b/2f292bbd742e369a100c91faa0483172cd91a1a422a6692055ac920946c5/pypiwin32-223-py3-none-any.whl

Here's the full log:

C:\saspy-hh_win32_com>pip install .
Processing c:\saspy-hh_win32_com
Requirement already satisfied: pygments in c:\programdata\anaconda3\lib\site-packages (from saspy==2.4.4)
Requirement already satisfied: ipython>=4.0.0 in c:\programdata\anaconda3\lib\site-packages (from saspy==2.4.4)
Collecting pypiwin32 (from saspy==2.4.4)
  Downloading https://files.pythonhosted.org/packages/d0/1b/2f292bbd742e369a100c91faa0483172cd91a1a422a6692055ac920946c5/pypiwin32-223-py3-none-any.whl
Collecting pywin32>=223 (from pypiwin32->saspy==2.4.4)
  Downloading https://files.pythonhosted.org/packages/b2/1a/7727b406391b0178b6ccb7e447e963df5ebf1ce9e0f615fc6ce23b6f6753/pywin32-224-cp36-cp36m-win_amd64.whl (9.1MB)
    100% |????????????????????????????????| 9.1MB 139kB/s
Building wheels for collected packages: saspy
  Running setup.py bdist_wheel for saspy ... done
  Stored in directory: C:\Users\sastpw\AppData\Local\pip\Cache\wheels\72\05\ed\587be254f815e0202e1d70172af09f342ac5106c299c68a86a
Successfully built saspy
Installing collected packages: pywin32, pypiwin32, saspy
  Found existing installation: pywin32 220
    DEPRECATION: Uninstalling a distutils installed project (pywin32) has been deprecated and will be removed in a future version. This is due to the fact that uninstalling a distu
tils project will only partially uninstall the project.
    Uninstalling pywin32-220:
Exception:
Traceback (most recent call last):
  File "C:\ProgramData\Anaconda3\lib\shutil.py", line 538, in move
    os.rename(src, real_dst)
PermissionError: [WinError 5] Access is denied: 'c:\\programdata\\anaconda3\\lib\\site-packages\\pywin32-220-py3.6.egg-info' -> 'C:\\Users\\sastpw\\AppData\\Local\\Temp\\pip-ajjpau
rd-uninstall\\programdata\\anaconda3\\lib\\site-packages\\pywin32-220-py3.6.egg-info'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\ProgramData\Anaconda3\lib\site-packages\pip\basecommand.py", line 215, in main
    status = self.run(options, args)
  File "C:\ProgramData\Anaconda3\lib\site-packages\pip\commands\install.py", line 342, in run
    prefix=options.prefix_path,
  File "C:\ProgramData\Anaconda3\lib\site-packages\pip\req\req_set.py", line 778, in install
    requirement.uninstall(auto_confirm=True)
  File "C:\ProgramData\Anaconda3\lib\site-packages\pip\req\req_install.py", line 754, in uninstall
    paths_to_remove.remove(auto_confirm)
  File "C:\ProgramData\Anaconda3\lib\site-packages\pip\req\req_uninstall.py", line 115, in remove
    renames(path, new_path)
  File "C:\ProgramData\Anaconda3\lib\site-packages\pip\utils\__init__.py", line 267, in renames
    shutil.move(old, new)
  File "C:\ProgramData\Anaconda3\lib\shutil.py", line 553, in move
    os.unlink(src)
PermissionError: [WinError 5] Access is denied: 'c:\\programdata\\anaconda3\\lib\\site-packages\\pywin32-220-py3.6.egg-info'
You are using pip version 9.0.1, however version 19.1 is available.
You should consider upgrading via the 'python -m pip install --upgrade pip' command.

C:\saspy-hh_win32_com>

@tomweber-sas
Copy link
Contributor

Ah, yep, now that I look at the syntax of it, I changed the line to be

      extras_require = {'win32' : ['pypiwin32']},

and it installs w/out the win32 install and succeeds, and when I install with [win32] added, it tries to install it (and fails, but that's not the problem). So cool, that does work as you described. Now I can test out the access method once I configure it!

@tomweber-sas
Copy link
Contributor

Well, I'll be. I got it to work and it connected and was successfully working with many methods! Good job!
I just tried running the saspy superdemo notebook with it: https://github.com/sassoftware/saspy-examples/blob/master/SAS_contrib/SGF_SuperDemo1.ipynb

That was run on a local system, so the cells (like read/write_csv) have local paths, so they didn't run. I'll try them out another time. I have an abend in df2sd() and I don't get the log from saslog(). Of course upload/download aren't found, and the html that comes back needs a couple tweaks I put in way back when to keep the formatting in Jupyter from getting out of whack. But, as a first pass, it's really pretty functional and works! That's cool.

Here's an html (download and remove the .txt. to render it; this fool thing won't let you attach .html files).
SGF_SuperDemo1_HarryCOM.html.txt

Again, this is a first pass and I haven't tried out lots of stuff. It will certainly need some tweaks and enhancements, but it looks pretty good to start with!

Tom

@cjdinger
Copy link
Member

cjdinger commented May 3, 2019

Just want to say thanks to @hhubbell for all of this work. I'm glad to see it happening, as then anyone with EG (or the SAS Integration Technologies client, which is a free download from SAS support) will be able to leverage saspy without hunting for Java jars. Great stuff!

@hhubbell
Copy link
Contributor Author

hhubbell commented May 8, 2019

I've made the following updates:

  1. Add Upload/Download methods. Added tests to validate this functionality.
  2. Made the pywin32 install optional on Windows clients. To install pywin32 in addition to saspy, issue the following pip command: pip install saspy[iomcom]. Naming the install option iomcom seemed more appropriate as you are "unlocking" the iomcom IO module when indicating that extra.
  3. Renamed the following configuration keys:
    • host -> iomhost
    • port -> iomport
    • user -> omruser
    • pw -> omrpw

@tomweber-sas, can you point me to the commit you are referencing when you say:

and the html that comes back needs a couple tweaks I put in way back when to keep the formatting in Jupyter from getting out of whack.

I can't figure out what change you're suggesting.

I have an abend in df2sd() and I don't get the log from saslog()

Can you provide an example of the dataframe that causes this? I'm not able to reproduce the error.

@tomweber-sas
Copy link
Contributor

Hey Harry, that's great. I'll pull this and run it through some paces. The exception in df2sd is in that html I attached above; SGF_SuperDemo1_HarryCOM.html.txt (download and delete '.txt' off it so it's '.html' and you can view it. Here's the short of it though; just the cars data set back and forth:

sas
Access Method         = COM
SAS Config name       = iomcom
WORK Path             = C:\Users\dbitest2\AppData\Local\Temp\SAS Temporary Files\_TD3177144_RDCESX51082_\Prc2\
SAS Version           = 9.04.01M6P08232018
SASPy Version         = 2.4.4
Teach me SAS          = False
Batch                 = False
Results               = HTML
SAS Session Encoding  = wlatin1
Python Encoding value = cp1252
SAS process Pid value = 3177144

cars = sas.sasdata('cars', libref='sashelp')
car_df = cars.to_df()

cars_full_circle = sas.df2sd(car_df, 'cfc')
---------------------------------------------------------------------------
com_error                                 Traceback (most recent call last)
<ipython-input-51-e490979f3500> in <module>()
----> 1 cars_full_circle = sas.df2sd(car_df, 'cfc')

C:\ProgramData\Anaconda3\lib\site-packages\saspy\sasbase.py in df2sd(self, df, table, libref, results, keep_outer_quotes)
    859         :return: SASdata object
    860         """
--> 861         return self.dataframe2sasdata(df, table, libref, results, keep_outer_quotes)
    862 
    863     def dataframe2sasdata(self, df: 'pd.DataFrame', table: str = '_df', libref: str = '',

C:\ProgramData\Anaconda3\lib\site-packages\saspy\sasbase.py in dataframe2sasdata(self, df, table, libref, results, keep_outer_quotes)
    884             return None
    885         else:
--> 886             self._io.dataframe2sasdata(df, table, libref, keep_outer_quotes)
    887 
    888         if self.exist(table, libref):

C:\ProgramData\Anaconda3\lib\site-packages\saspy\sasiocom.py in dataframe2sasdata(self, df, table, libref, keep_outer_quotes)
    544 
    545         self.adodb.Execute(sql_create)
--> 546         self.adodb.Execute(sql_insert)
    547 
    548     def sasdata2dataframe(self, table: str, libref: str=None, dsopts: dict=None, method: str='', **kwargs) -> 'pd.DataFrame':

C:\ProgramData\Anaconda3\lib\site-packages\win32com\client\dynamic.py in Execute(self, CommandText, RecordsAffected, Options)

C:\ProgramData\Anaconda3\lib\site-packages\win32com\client\dynamic.py in _ApplyTypes_(self, dispid, wFlags, retType, argTypes, user, resultCLSID, *args)
    285 
    286         def _ApplyTypes_(self, dispid, wFlags, retType, argTypes, user, resultCLSID, *args):
--> 287                 result = self._oleobj_.InvokeTypes(*(dispid, LCID, wFlags, retType, argTypes) + args)
    288                 return self._get_good_object_(result, user, resultCLSID)
    289 

com_error: (-2147352567, 'Exception occurred.', (0, '', '<?xml version="1.0" ?><Exceptions><Exception><SASMessage severity="Error">SQL passthru expression contained these errors: ERROR 22-322: Syntax error, expecting one of the following: a quoted string, a numeric constant, a datetime constant, a missing value, ), +, \',\', -, MISSING, NULL, USER....  ERROR 202-322: The option or parameter is not recognized and will be ignored....  ERROR 22-322: Syntax error, expecting one of the following: a quoted string, a numeric constant, a datetime constant, a missing value, ), +, \',\', -, MISSING, NULL, USER....  ERROR 202-322: The option or parameter is not recognized and will be ignored.</SASMessage></Exception></Exceptions>', None, 0, -2147217900), None)

It's just round tripping the cars data set. I don't know if it could be a version issue with the win32 module I have? See if you can do the same w/out this or if you get it too.

I'll dig up the tweaks to the html and send you that.

Thanks!
Tom

@tomweber-sas
Copy link
Contributor

The html tweak is the following:

      lstd = lstf.replace(chr(12), chr(10)).replace('<body class="c body">',
                                                    '<body class="l body">').replace("font-size: x-small;",
                                                                                     "font-size:  normal;")

So in your code, I added it here in your submit method:

        # Retrieve listing and log
        log = self._getlog()
        if results.lower() == 'html':
            listing = self._getfile(self._gethtmlfn(), decode=True)
            listing = listing.replace(chr(12), chr(10)).replace('<body class="c body">',
	                                                        '<body class="l body">').replace("font-size: x-small;",
	                                                                                         "font-size:  normal;")
        else:
            listing = self._getlst()

And it fixes the notebook from looking like this:
image

to looking like this:
image

I have that in all of my access methods. For Jupyter Notebook, it was necessary to make it present well. Without it, the whole web page of the jupyter notebook is affected.

Tom

@tomweber-sas
Copy link
Contributor

I'm confused at how those two posts from me got out of order and the first one (second now) says:
tomweber-sas commented 6 hours from now

???
Tom

@hhubbell
Copy link
Contributor Author

hhubbell commented May 8, 2019

It looks like the df2sd issue was related to nan values. When sd2df loads a record with NULL values, pandas coerces the value to nan. When writing the dataframe back, these values need to be mapped back to NULL.

The HTML formatting changes were also applied.

@tomweber-sas
Copy link
Contributor

Cool, thanks Harry. Yes, it smelled like a missing values issue. The age old SAS I/O conversion issue; been dealing with that for 30+ years now myself :)
I'll pull your latest and try it out.
Thanks!
Tom

@tomweber-sas
Copy link
Contributor

Got a new error with this. Did this change with the version of Pandas? I see it in some doc but not others.
Also, saslog() returns ''.


AttributeError Traceback (most recent call last)
in ()
----> 1 cars_full_circle = sas.df2sd(car_df, 'cfc')

C:\ProgramData\Anaconda3\lib\site-packages\saspy\sasbase.py in df2sd(self, df, table, libref, results, keep_outer_quotes)
859 :return: SASdata object
860 """
--> 861 return self.dataframe2sasdata(df, table, libref, results, keep_outer_quotes)
862
863 def dataframe2sasdata(self, df: 'pd.DataFrame', table: str = '_df', libref: str = '',

C:\ProgramData\Anaconda3\lib\site-packages\saspy\sasbase.py in dataframe2sasdata(self, df, table, libref, results, keep_outer_quotes)
884 return None
885 else:
--> 886 self._io.dataframe2sasdata(df, table, libref, keep_outer_quotes)
887
888 if self.exist(table, libref):

C:\ProgramData\Anaconda3\lib\site-packages\saspy\sasiocom.py in dataframe2sasdata(self, df, table, libref, keep_outer_quotes)
546 for i, col in enumerate(row):
547 func = formats[df.columns[i]]
--> 548 vals.append(func(col))
549
550 sql_values.append('values({})'.format(', '.join(vals)))

C:\ProgramData\Anaconda3\lib\site-packages\saspy\sasiocom.py in (x)
537 length = df[name].map(str).map(len).max()
538 definition = "'{}'n char({})".format(name, length)
--> 539 formats[name] = lambda x: "'{}'".format(x) if pd.isna(x) is False else 'NULL'
540
541 columns.append(definition)

AttributeError: module 'pandas' has no attribute 'isna'

@hhubbell
Copy link
Contributor Author

hhubbell commented May 8, 2019

Which version of pandas do you have installed on your system? The pandas I have installed supports isna - not sure when they added it.

https://pandas.pydata.org/pandas-docs/version/0.23.4/generated/pandas.isna.html

EDIT: Ah, according to their git history it was at least 2 years ago.

@tomweber-sas
Copy link
Contributor

FYI:

C:\saspy-hh_win32_com>pip show pandas
Name: pandas
Version: 0.19.2
Summary: Powerful data structures for data analysis, time series,and statistics
Home-page: http://pandas.pydata.org
Author: The PyData Development Team
Author-email: pydata@googlegroups.com
License: BSD
Location: c:\programdata\anaconda3\lib\site-packages
Requires: python-dateutil, pytz, numpy
You are using pip version 9.0.1, however version 19.1 is available.
You should consider upgrading via the 'python -m pip install --upgrade pip' command.

C:\saspy-hh_win32_com>

`isna` is an alias for `isnull`, added to the pandas library in October, 2017.
@hhubbell
Copy link
Contributor Author

hhubbell commented May 8, 2019

Ok, looks like we can use isnull instead to support your archaic version of pandas :)

isna is just an alias for isnull in the pandas library

@tomweber-sas
Copy link
Contributor

cool, and yeah, I know. :) I'm in the same boat trying to make things work w/ old and new "other people's code".

@tomweber-sas
Copy link
Contributor

df2sd now works for me too! How about that saslog()? I'll try out the others like the csv and up/download tomorrow. This is looking really good. Also, we will need a section in the config doc for this, especially with details about how to get the class_id and what 'provider' means or what options there are.
Ijust copied what you had and it worked. I don't know anything about those myself.

@cjdinger
Copy link
Member

cjdinger commented May 8, 2019

By class ID, do you mean the values you get from:

proc iomoperate; list types; quit;

Per https://blogs.sas.com/content/sasdummy/2013/02/22/using-windows-powershell-to-connect-to-a-sas-metadata-server/

@hhubbell
Copy link
Contributor Author

hhubbell commented May 8, 2019

@tomweber-sas
Ok, great. I will work on some documentation.

The issue with saslog() is related to how the LanguageService COM object consumes the log from the server. Whenever we call FlushLog, the entire log that's been spooled is flushed to the client. Since we call FlushLog on every submit, the server doesn't have anything left to give us when we go to call saslog() again.

>>> ll = sas.submit("%put hello world!;")
>>> ll.keys()
['LOG', 'LST']

>>> ll['LOG'] # Got the log automatically on `submit`
'[Entire log message here]'

>>> sas.saslog() #Nothing left to flush from the server, since we haven't submitted anything else.

We can work around this behavior by having the iomcom module hang on to the most recent log message in a cache. How does that sound?

@cjdinger
Yes, exactly! That Class ID value is what we plug in to the configuration file for this module to connect to the provider. I'm pretty sure that value is static for all installations - maybe you can confirm? At least, the Class ID value in that demo is one I've seen in a couple places with proc iomoperate.

@tomweber-sas
Copy link
Contributor

Harry, for saslog() you can do what I do in the STDIO interface; I just append each part to continuous log hung off the session. So I just return that for the saslog() call.

@tomweber-sas
Copy link
Contributor

Two little thing I've found to fix.

  1. read_csv get's an error because the path on the filename isn't quoted, but it is in write_csv; just add the quotes in the generated code.
        proc_code = """
            filename csv_file '{}';
            proc import datafile=csv_file out={} dbms=csv replace;
                {}
            run;
        """.format(filepath, tablepath, self._sb._impopts(opts))
  1. download isn't checking to see if local is a dir and then adding the filename from the remote file:
        if os.path.isdir(local):
           local = local + os.sep + remote.rpartition(self._sb.hostsep)[2]

And I get the saslog now :) Which I needed to see the non-quoted filename error.

Looking good!
Tom

1. Fix filename syntax error in `read_csv`. Wrap in quotes and escape quotes in path.
2. Escape quotes in `write_csv` file path.
3. Support writing to directory in `download`.
@hhubbell
Copy link
Contributor Author

Hi Tom,

I've resolved the outstanding issues and updated the documentation.

@tomweber-sas
Copy link
Contributor

Hey Harry, that's all looking good. I see you had version 3.1.0 in the doc. I think that's good. I'll go ahead and snap a 3.0.0 version off before merging it in and bump the version to 3.1.0 for this.

Can you add just a couple things in the doc.

  1. add an example of using proc iomoperate, because that'll be the first question they will have.
    is this the right example (show the results too so it's clear what to use from the output:
/* submit this to get the workspace server Class Identifier to use for 'class-id' */
proc iomoperate;
   list types;
quit;


SAS Workspace Server 
    Short type name  : Workspace 
    Class identifier : 440196d4-90f0-11d0-9f41-00a024bb830c
  1. Also, a bit on requirements and limitations. This requires that you have EG installed on the machine your using, or is there other ways it can work? I know it's only for Windows clients, are there any other requirements? And I see it says 'from windows to remote 9.4'. Can it not work with a local connection? Or is that a bit of an unknown at the moment?

Thanks,
Tom

@hhubbell
Copy link
Contributor Author

Hi Tom,

Added proc iomoperate example and listed the EG/Integration Technologies Client requirement.

I mention in the documentation that remote connection are supported just because I simply don't know if a local connection would work. There may be some additional changes required to support local connections, or it simply may not be possible. I don't know enough about how a local install differs from a remote install to say for sure - for instance, does a local install expose an open IOM connection port? I only have a remote instance to test against.

Thanks!

@cjdinger
Copy link
Member

To enable, you should be able to download the SAS Integration Technologies client -- free from our website on support.sas.com, Demos and Downloads. EG and AMO users will already have it, as will Base SAS on Windows. But "greenfield" users can set themselves up for free -- just need a SAS to connect to.

In terms of connecting to local SAS without a port, that's possible with a local COM connection. See my C# example here for guidance.

@hhubbell
Copy link
Contributor Author

@cjdinger, what is the enum value for Protocols.ProtocolCom?

obServer.Protocol = SASObjectManager.Protocols.ProtocolCom;

It looks like this might be the only update needed to support local connections.

@tomweber-sas
Copy link
Contributor

Looks like if this works, we can support both remote and local, like the current IOM access method.
That would change the requirements for the following:
Remote (requires):
iomhost, iomport class_id

Local (requires):
don't specify any of iomhost, iomport class_id

Then the code would key off 'provider' instead of 'class_id' because that's require for both cases (in sasbase.py SASconfig)?

and the code in the access method would be:

Remote (includes this):
obServer.Protocol = SASObjectManager.Protocols.ProtocolBridge;
obServer.ClassIdentifier = "440196d4-90f0-11d0-9f41-00a024bb830c";
obServer.MachineDNSName = Host;
obServer.Port = Convert.ToInt32(Port);

Local (includes this):
obServer.Protocol = SASObjectManager.Protocols.ProtocolCom;
obServer.MachineDNSName = "localhost";
obServer.Port = 0;

All the rest would be the same, just those lines different it seems?

If that's all it takes, that would be good to be able to support both cases.
Tom

@tomweber-sas
Copy link
Contributor

Harry, I changed the following in your access method and it worked:

        if self.sascfg.host:
           server.MachineDNSName = self.sascfg.host
           server.Port = self.sascfg.port
           server.Protocol = self.IOM_PROTOCOL
           server.ClassIdentifier = self.sascfg.class_id
           self.workspace = factory.CreateObjectByServer(self.SAS_APP, True,
                                                         server, self.sascfg.user, self.sascfg.pw)

        else:
           server.MachineDNSName = '127.0.0.1'
           server.Port = 0
           server.Protocol = 0
           self.workspace = factory.CreateObjectByServer(self.SAS_APP, True, server, None, None)

with that, and changing sasbase SASconfig to get 'provider' instead of 'class_id' as the key to use the sasiocom AM, and the doc changes for local vs. remote (like the existing IOM does, this should be good to go!

Also, omruser/pw are also not required for local, so really only 'provider' to specify that and trigger it.

Tom

@hhubbell
Copy link
Contributor Author

Looks like ProtocolCom is the default (0), so I think that commit should work.

Let me know if you can get it running with a local install - I don't have access to one so I'm not able to test.

@hhubbell
Copy link
Contributor Author

Perfect timing!

@tomweber-sas
Copy link
Contributor

Cool, though I would make host be the trigger for remote and it's absense be loacal, so it's consistent w/ the current IOM. And for that code, set user/pw to None in the local case, so they are correct on the connection call.

@tomweber-sas
Copy link
Contributor

I also like to use '127.0.0.1' instead of 'localhost', only because I've see some odd cases where localhost alias isn't set up right. It is for me and I've never had that, but others on occasion. But 127.0.0.1 is always correct.

This is really close!, it's looking good!
Tom

@hhubbell
Copy link
Contributor Author

Fixed

@tomweber-sas
Copy link
Contributor

Well, that all works on my system for both local and remote. This looks good to merge in to me!
You ready!??

Any last changes?
Tom

@hhubbell
Copy link
Contributor Author

Ready! Thanks Tom!

@tomweber-sas tomweber-sas merged commit 81bd07b into sassoftware:master May 16, 2019
@hhubbell hhubbell deleted the hh_win32_com branch May 16, 2019 19:20
@tomweber-sas
Copy link
Contributor

This is V3.1.0 and is the current version out on pypi, and should soon be building for conda!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants