From 601fb4fd51f6a5a423c44f6e45cabb6ab42c2884 Mon Sep 17 00:00:00 2001 From: Shuguang Sun Date: Sat, 13 Apr 2024 23:32:16 +0800 Subject: [PATCH] feat: add library support for SASPy profile Signed-off-by: Shuguang Sun --- client/src/commands/authorize.ts | 4 + .../LibraryNavigator/LibraryAdapterFactory.ts | 3 + client/src/components/profile.ts | 30 +- client/src/connection/index.ts | 2 +- client/src/connection/saspy/CodeRunner.ts | 72 +++++ .../connection/saspy/SaspyLibraryAdapter.ts | 298 ++++++++++++++++++ client/src/connection/saspy/index.ts | 83 +++-- client/src/connection/saspy/types.ts | 9 + connect-and-run.md | 39 ++- 9 files changed, 471 insertions(+), 69 deletions(-) create mode 100644 client/src/connection/saspy/CodeRunner.ts create mode 100644 client/src/connection/saspy/SaspyLibraryAdapter.ts diff --git a/client/src/commands/authorize.ts b/client/src/commands/authorize.ts index 9a1b1d090..b8debbdab 100644 --- a/client/src/commands/authorize.ts +++ b/client/src/commands/authorize.ts @@ -43,6 +43,10 @@ export const checkProfileAndAuthorize = commands.executeCommand("setContext", "SAS.librariesDisplayed", true); libraryNavigator.refresh(); return finishAuthorization(profileConfig); + case ConnectionType.SASPY: + commands.executeCommand("setContext", "SAS.librariesDisplayed", true); + libraryNavigator.refresh(); + return finishAuthorization(profileConfig); default: return finishAuthorization(profileConfig); } diff --git a/client/src/components/LibraryNavigator/LibraryAdapterFactory.ts b/client/src/components/LibraryNavigator/LibraryAdapterFactory.ts index c4e354339..fedb5e753 100644 --- a/client/src/components/LibraryNavigator/LibraryAdapterFactory.ts +++ b/client/src/components/LibraryNavigator/LibraryAdapterFactory.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import ItcLibraryAdapter from "../../connection/itc/ItcLibraryAdapter"; import RestLibraryAdapter from "../../connection/rest/RestLibraryAdapter"; +import SaspyLibraryAdapter from "../../connection/saspy/SaspyLibraryAdapter"; import { ConnectionType } from "../profile"; import { LibraryAdapter } from "./types"; @@ -11,6 +12,8 @@ class LibraryAdapterFactory { case ConnectionType.IOM: case ConnectionType.COM: return new ItcLibraryAdapter(); + case ConnectionType.SASPY: + return new SaspyLibraryAdapter(); case ConnectionType.Rest: default: return new RestLibraryAdapter(); diff --git a/client/src/components/profile.ts b/client/src/components/profile.ts index 1e7704adc..4c10cdf3f 100644 --- a/client/src/components/profile.ts +++ b/client/src/components/profile.ts @@ -20,7 +20,7 @@ enum ConnectionOptions { SAS9COM = "SAS 9.4 (local)", SAS9IOM = "SAS 9.4 (remote - IOM)", SAS9SSH = "SAS 9.4 (remote - SSH)", - SAS9SASPY = "SASPY", + SAS9SASPY = "SAS 9.4 (SASPy)", SASViya = "SAS Viya", } @@ -95,7 +95,6 @@ export interface SSHProfile extends BaseProfile { export interface SASPYProfile extends BaseProfile { connectionType: ConnectionType.SASPY; - // saspath: string; cfgname: string; pythonpath: string; } @@ -112,7 +111,12 @@ export interface IOMProfile extends BaseProfile { port: number; } -export type Profile = ViyaProfile | SSHProfile | SASPYProfile | COMProfile | IOMProfile; +export type Profile = + | ViyaProfile + | SSHProfile + | SASPYProfile + | COMProfile + | IOMProfile; export enum AutoExecType { File = "file", @@ -601,24 +605,8 @@ export class ProfileConfig { await this.upsertProfile(name, profileClone); } else if (profileClone.connectionType === ConnectionType.SASPY) { - // profileClone.pythonpath = await createInputTextBox( - // ProfilePromptType.PYTHONPath, - // profileClone.pythonpath, - // ); - // if (profileClone.pythonpath === undefined) { - // return; - // } - - // profileClone.cfgname = await createInputTextBox( - // ProfilePromptType.Cfgname, - // profileClone.cfgname, - // ); - // if (profileClone.cfgname === undefined) { - // return; - // } - profileClone.cfgname = ""; - // profileClone.sasOptions = []; + // profileClone.sasOptions = []; await this.upsertProfile(name, profileClone); } else if (profileClone.connectionType === ConnectionType.COM) { profileClone.sasOptions = []; @@ -697,7 +685,7 @@ export enum ProfilePromptType { Port, Username, Cfgname, - PYTHONpath + PYTHONpath, } /** diff --git a/client/src/connection/index.ts b/client/src/connection/index.ts index b32f6ff3b..d56053403 100644 --- a/client/src/connection/index.ts +++ b/client/src/connection/index.ts @@ -17,9 +17,9 @@ import { LogLine as ComputeLogLine, LogLineTypeEnum as ComputeLogLineTypeEnum, } from "./rest/api/compute"; +import { getSession as getSASPYSession } from "./saspy"; import { Session } from "./session"; import { getSession as getSSHSession } from "./ssh"; -import { getSession as getSASPYSession } from "./saspy"; let profileConfig: ProfileConfig; diff --git a/client/src/connection/saspy/CodeRunner.ts b/client/src/connection/saspy/CodeRunner.ts new file mode 100644 index 000000000..8cbc41e10 --- /dev/null +++ b/client/src/connection/saspy/CodeRunner.ts @@ -0,0 +1,72 @@ +// Copyright © 2024, SAS Institute Inc., Cary, NC, USA. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { commands } from "vscode"; + +import { SASPYSession } from "."; +import { LogLine, getSession } from ".."; +import { useRunStore } from "../../store"; + +class CodeRunner { + public async runCode( + code: string, + startTag: string = "", + endTag: string = "", + ): Promise { + // If we're already executing code, lets wait for it + // to finish up. + let unsubscribe; + if (useRunStore.getState().isExecutingCode) { + await new Promise((resolve) => { + unsubscribe = useRunStore.subscribe( + (state) => state.isExecutingCode, + (isExecutingCode) => !isExecutingCode && resolve(true), + ); + }); + } + + const { setIsExecutingCode } = useRunStore.getState(); + setIsExecutingCode(true); + commands.executeCommand("setContext", "SAS.running", true); + const session = getSession(); + + let logText = ""; + const onExecutionLogFn = session.onExecutionLogFn; + const outputLines = []; + const addLine = (logLines: LogLine[]) => + outputLines.push(...logLines.map(({ line }) => line)); + + try { + await session.setup(true); + + // Lets capture output to use it on + session.onExecutionLogFn = addLine; + + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + await (session as SASPYSession).run(code); + + const logOutput = outputLines.filter((line) => line.trim()).join(""); + + logText = + startTag && endTag + ? logOutput + .slice( + logOutput.lastIndexOf(startTag), + logOutput.lastIndexOf(endTag), + ) + .replace(startTag, "") + .replace(endTag, "") + : logOutput; + } finally { + unsubscribe && unsubscribe(); + // Lets update our session to write to the log + session.onExecutionLogFn = onExecutionLogFn; + + setIsExecutingCode(false); + commands.executeCommand("setContext", "SAS.running", false); + } + + return logText; + } +} + +export default CodeRunner; diff --git a/client/src/connection/saspy/SaspyLibraryAdapter.ts b/client/src/connection/saspy/SaspyLibraryAdapter.ts new file mode 100644 index 000000000..3eeb88f1b --- /dev/null +++ b/client/src/connection/saspy/SaspyLibraryAdapter.ts @@ -0,0 +1,298 @@ +// Copyright © 2024, SAS Institute Inc., Cary, NC, USA. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { commands, l10n } from "vscode"; + +import { ChildProcessWithoutNullStreams } from "child_process"; + +import { onRunError } from "../../commands/run"; +import { + LibraryAdapter, + LibraryItem, + TableData, + TableRow, +} from "../../components/LibraryNavigator/types"; +import { Column, ColumnCollection } from "../rest/api/compute"; +import CodeRunner from "./CodeRunner"; +import { Config } from "./types"; + +class SaspyLibraryAdapter implements LibraryAdapter { + protected hasEstablishedConnection: boolean = false; + protected shellProcess: ChildProcessWithoutNullStreams; + protected pollingForLogResults: boolean = false; + protected log: string[] = []; + protected endTag: string = ""; + protected outputFinished: boolean = false; + protected config: Config; + protected codeRunner = new CodeRunner(); + + public async connect(): Promise { + this.hasEstablishedConnection = true; + } + + public async setup(): Promise { + if (this.hasEstablishedConnection) { + return; + } + + await this.connect(); + } + + public async deleteTable(item: LibraryItem): Promise { + const code = ` + proc datasets library=${item.library} nolist nodetails; delete ${item.name}; run; + `; + + await this.runCode(code); + } + + public async getColumns(item: LibraryItem): Promise { + const sql = ` + %let OUTPUT; + proc sql; + select catx(',', name, type, varnum) as column into: OUTPUT separated by '~' + from sashelp.vcolumn + where libname='${item.library}' and memname='${item.name}' + order by varnum; + quit; + %put ; %put &OUTPUT; %put ; + `; + + const columnLines = processQueryRows( + await this.runCode(sql, "", ""), + ); + + const columns = columnLines.map((lineText): Column => { + const [name, type, index] = lineText.split(","); + + return { + name, + type, + index: parseInt(index, 10), + }; + }); + + return { + items: columns, + count: -1, + }; + } + + public async getLibraries(): Promise<{ + items: LibraryItem[]; + count: number; + }> { + const sql = ` + %let OUTPUT; + proc sql; + select catx(',', libname, readonly) as libname_target into: OUTPUT separated by '~' + from sashelp.vlibnam order by libname asc; + quit; + %put ; %put &OUTPUT; %put ; + `; + + const libNames = processQueryRows( + await this.runCode(sql, "", ""), + ); + + const libraries = libNames.map((lineText): LibraryItem => { + const [libName, readOnlyValue] = lineText.split(","); + + return { + type: "library", + uid: libName, + id: libName, + name: libName, + readOnly: readOnlyValue === "yes", + }; + }); + + return { + items: libraries, + count: -1, + }; + } + + public async getRows( + item: LibraryItem, + start: number, + limit: number, + ): Promise { + const { rows: rawRowValues, count } = await this.getDatasetInformation( + item, + start, + limit, + ); + + const rows = rawRowValues.map((line, idx: number): TableRow => { + const rowData = [`${start + idx + 1}`].concat(line); + return { cells: rowData }; + }); + + return { + rows, + count, + }; + } + + public async getRowsAsCSV( + item: LibraryItem, + start: number, + limit: number, + ): Promise { + // We only need the columns for the first page of results + const columns = + start === 0 + ? { + columns: ["INDEX"].concat( + (await this.getColumns(item)).items.map((column) => column.name), + ), + } + : {}; + + const { rows } = await this.getRows(item, start, limit); + + rows.unshift(columns); + + // Fetching csv doesn't rely on count. Instead, we get the count + // upfront via getTableRowCount + return { rows, count: -1 }; + } + + public async getTableRowCount( + item: LibraryItem, + ): Promise<{ rowCount: number; maxNumberOfRowsToRead: number }> { + const code = ` + proc sql; + SELECT COUNT(1) into: COUNT FROM ${item.library}.${item.name}; + quit; + %put &COUNT; + `; + + const output = await this.runCode(code, "", ""); + const rowCount = parseInt(output.replace(/[^0-9]/g, ""), 10); + + return { rowCount, maxNumberOfRowsToRead: 100 }; + } + + public async getTables(item: LibraryItem): Promise<{ + items: LibraryItem[]; + count: number; + }> { + const sql = ` + %let OUTPUT; + proc sql; + select memname into: OUTPUT separated by '~' + from sashelp.vtable + where libname='${item.name!}' + order by memname asc; + quit; + %put ; %put &OUTPUT; %put ; + `; + + const tableNames = processQueryRows( + await this.runCode(sql, "", ""), + ); + + const tables = tableNames.map((lineText): LibraryItem => { + const [table] = lineText.split(","); + + return { + type: "table", + uid: `${item.name!}.${table}`, + id: table, + name: table, + library: item.name, + readOnly: item.readOnly, + }; + }); + + return { items: tables, count: -1 }; + } + + protected async getDatasetInformation( + item: LibraryItem, + start: number, + limit: number, + ): Promise<{ rows: Array; count: number }> { + const maxTableNameLength = 32; + const tempTable = `${item.name}${hms()}${start}`.substring( + 0, + maxTableNameLength, + ); + const code = ` + options nonotes nosource nodate nonumber; + %let COUNT; + proc sql; + SELECT COUNT(1) into: COUNT FROM ${item.library}.${item.name}; + quit; + data work.${tempTable}; + set ${item.library}.${item.name}; + if ${start + 1} <= _N_ <= ${start + limit} then output; + run; + + filename out temp; + proc json nokeys out=out pretty; export work.${tempTable}; run; + + %put ; + %put &COUNT; + data _null_; infile out; input; put _infile_; run; + %put ; + proc datasets library=work nolist nodetails; delete ${tempTable}; run; + options notes source date number; + `; + + let output = await this.runCode(code, "", ""); + + // Extract result count + const countRegex = /(.*)<\/Count>/; + const countMatches = output.match(countRegex); + const count = parseInt(countMatches[1].replace(/\s|\n/gm, ""), 10); + output = output.replace(countRegex, ""); + + const rows = output.replace(/\n|\t/gm, ""); + try { + const tableData = JSON.parse(rows); + return { rows: tableData[`SASTableData+${tempTable}`], count }; + } catch (e) { + console.warn("Failed to load table data with error", e); + console.warn("Raw output", rows); + throw new Error( + l10n.t( + "An error was encountered when loading table data. This usually happens when a table is too large or the data couldn't be processed. See console for more details.", + ), + ); + } + } + + protected async runCode( + code: string, + startTag: string = "", + endTag: string = "", + ): Promise { + try { + return await this.codeRunner.runCode(code, startTag, endTag); + } catch (e) { + onRunError(e); + commands.executeCommand("setContext", "SAS.librariesDisplayed", false); + return ""; + } + } +} + +const processQueryRows = (response: string): string[] => { + const processedResponse = response.trim().replace(/\n|\t/gm, ""); + if (!processedResponse) { + return []; + } + + return processedResponse + .split("~") + .filter((value, index, array) => array.indexOf(value) === index); +}; + +const hms = () => { + const date = new Date(); + return `${date.getHours()}${date.getMinutes()}${date.getSeconds()}`; +}; + +export default SaspyLibraryAdapter; diff --git a/client/src/connection/saspy/index.ts b/client/src/connection/saspy/index.ts index 77049221b..70376d486 100644 --- a/client/src/connection/saspy/index.ts +++ b/client/src/connection/saspy/index.ts @@ -4,15 +4,16 @@ import { Uri, workspace } from "vscode"; import { ChildProcessWithoutNullStreams, spawn } from "child_process"; -import { BaseConfig, LogLineTypeEnum, RunResult } from ".."; -import { - getGlobalStorageUri -} from "../../components/ExtensionContext"; +import { LogLineTypeEnum, RunResult } from ".."; +import { getGlobalStorageUri } from "../../components/ExtensionContext"; import { updateStatusBarItem } from "../../components/StatusBarItem"; -import { extractOutputHtmlFileName, wrapCodeWithOutputHtml } from "../../components/utils/sasCode"; +import { + extractOutputHtmlFileName, + wrapCodeWithOutputHtml, +} from "../../components/utils/sasCode"; import { Session } from "../session"; // import { scriptContent } from "./script"; -import { LineCodes } from "./types"; +import { Config, LineCodes } from "./types"; const LogLineTypes: LogLineTypeEnum[] = [ "normal", @@ -29,14 +30,6 @@ const LogLineTypes: LogLineTypeEnum[] = [ let sessionInstance: SASPYSession; -/** - * Configuration parameters for this connection provider - */ -export interface Config extends BaseConfig { - cfgname: string; - pythonpath: string; -} - export class SASPYSession extends Session { private _config: Config; private _shellProcess: ChildProcessWithoutNullStreams; @@ -46,7 +39,7 @@ export class SASPYSession extends Session { private _workDirectory: string; private _pollingForLogResults: boolean; private _logLineType = 0; - + public set config(value: Config) { this._config = value; } @@ -73,10 +66,10 @@ export class SASPYSession extends Session { //shell: true, // env: process.env, env: { - ...process.env, - //PATH: process.env.PATH + require('path').delimiter + __dirname, - PYTHONIOENCODING: "utf-8" - } + ...process.env, + //PATH: process.env.PATH + require('path').delimiter + __dirname, + PYTHONIOENCODING: "utf-8", + }, }, ); this._shellProcess.stdout.on("data", this.onShellStdOut); @@ -87,11 +80,13 @@ export class SASPYSession extends Session { %let rc = %sysfunc(dlgcdir("&workDir")); run; `; - const saspyWorkDirWithODS = wrapCodeWithOutputHtml(saspyWorkDir); - const saspyHtmlStyle = saspyWorkDirWithODS.match(/style=([^ ]+) /)?.[1] ?? "Illuminate"; + const saspyWorkDirWithODS = wrapCodeWithOutputHtml(saspyWorkDir); + const saspyHtmlStyle = + saspyWorkDirWithODS.match(/style=([^ ]+) /)?.[1] ?? "Illuminate"; - const cfgname = this._config.cfgname?.length > 0 ? this._config.cfgname : ""; - const scriptContent = ` + const cfgname = + this._config.cfgname?.length > 0 ? this._config.cfgname : ""; + const scriptContent = ` import saspy _cfgname = "${cfgname}" @@ -131,7 +126,7 @@ ll=sas.submit(vscode_saspy_code) `; - this._shellProcess.stdin.write(scriptContent + "\n", this.onWriteComplete); + this._shellProcess.stdin.write(scriptContent + "\n", this.onWriteComplete); // this._shellProcess.stdin.write( // "$runner = New-Object -TypeName SASRunner\n", // this.onWriteComplete, @@ -143,8 +138,7 @@ ll=sas.submit(vscode_saspy_code) * will not exist. The work dir should only be deleted when close is invoked. */ if (!this._workDirectory) { - - this._shellProcess.stdin.write(`sas\n`); + this._shellProcess.stdin.write(`sas\n`); if (this._config.sasOptions?.length > 0) { const sasOptsInput = `$sasOpts=${this.formatSASOptions( @@ -183,10 +177,7 @@ ll=sas.submit(vscode_saspy_code) /\bods html5\(id=vscode\)([^;]*;)/i, `ods html5(id=vscode) path="${this._workDirectory}"$1`, ); - const codeWithODSPath2 = codeWithODSPath.replace( - /\bods _all_ close;/i, - ``, - ); + const codeWithODSPath2 = codeWithODSPath.replace(/\bods _all_ close;/i, ``); //write an end mnemonic so that the handler knows when execution has finished const codeWithEnd = `${codeWithODSPath2}\n%put ${LineCodes.RunEndCode};`; @@ -200,13 +191,16 @@ ${codeWithEnd} this._html5FileName = ""; this._shellProcess.stdin.write(codeToRun); this._pollingForLogResults = true; - await this._shellProcess.stdin.write(`ll=sas.submit(codeToRun, results='HTML')\n`, async (error) => { - if (error) { - this._runReject(error); - } + await this._shellProcess.stdin.write( + `ll=sas.submit(codeToRun, results='HTML')\n`, + async (error) => { + if (error) { + this._runReject(error); + } - await this.fetchLog(); - }); + await this.fetchLog(); + }, + ); return runPromise; }; @@ -239,7 +233,7 @@ ${codeWithEnd} */ public cancel = async () => { this._pollingForLogResults = false; - this._shellProcess.stdin.write("print(r'abc')\n", async (error) => { + this._shellProcess.stdin.write("print(r'abc')\n", async (error) => { if (error) { this._runReject(error); } @@ -264,7 +258,7 @@ ${codeWithEnd} * writing each chunk to stdout. */ private fetchLog = async (): Promise => { - this._shellProcess.stdin.write(`print(ll['LOG'])\n`,this.onWriteComplete,); + this._shellProcess.stdin.write(`print(ll['LOG'])\n`, this.onWriteComplete); }; /** @@ -272,7 +266,7 @@ ${codeWithEnd} * @param chunk a buffer of stderr output from the child process. */ private onShellStdErr = (chunk: Buffer): void => { - const msg = chunk.toString('utf8'); + const msg = chunk.toString("utf8"); console.warn("shellProcess stderr: " + msg); if (/[^.> ]/.test(msg)) { this._runReject( @@ -282,7 +276,11 @@ ${codeWithEnd} ); } // If we encountered an error in setup, we need to go through everything again - if (/^We failed in getConnection|Setup error|spawn .+ ENOENT: Error/i.test(msg)) { + if ( + /^We failed in getConnection|Setup error|spawn .+ ENOENT: Error/i.test( + msg, + ) + ) { this._shellProcess.kill(); this._workDirectory = undefined; } @@ -385,6 +383,7 @@ ${codeWithEnd} */ private fetchResults = async () => { if (!this._html5FileName) { + this._pollingForLogResults = false; return this._runResolve({}); } @@ -448,9 +447,7 @@ ${LineCodes.ResultsFetchedCode} * @param c Instance denoting configuration parameters for this connection profile. * @returns created COM session. */ -export const getSession = ( - c: Partial, -): Session => { +export const getSession = (c: Partial): Session => { const defaults = { cfgname: "", pythonpath: "python", diff --git a/client/src/connection/saspy/types.ts b/client/src/connection/saspy/types.ts index 0e4734bfc..5e044a652 100644 --- a/client/src/connection/saspy/types.ts +++ b/client/src/connection/saspy/types.ts @@ -1,5 +1,6 @@ // Copyright © 2023, SAS Institute Inc., Cary, NC, USA. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { BaseConfig } from ".."; export enum LineCodes { ResultsFetchedCode = "--vscode-sas-extension-results-fetched--", @@ -8,3 +9,11 @@ export enum LineCodes { SessionCreatedCode = "--vscode-sas-extension-session-created--", LogLineType = "--vscode-sas-extension-log-line-type--", } + +/** + * Configuration parameters for this connection provider + */ +export interface Config extends BaseConfig { + cfgname: string; + pythonpath: string; +} diff --git a/connect-and-run.md b/connect-and-run.md index fb47f0ee6..c14549988 100644 --- a/connect-and-run.md +++ b/connect-and-run.md @@ -188,12 +188,29 @@ Note: the default path to the SAS executable (saspath) is /opt/sasinside/SASHome 6. Add the public part of the keypair to the SAS server. Add the contents of the key file to the ~/.ssh/authorized_keys file. +## Profile: SAS 9.4 (SASPy) + +In order to use this connection type, you'll need to have Python and [SASPy](https://sassoftware.github.io/saspy) installed locally (the machine VS Code is installed on) or remotely (if you are using VScode remote development). SASPy fully supports connection methods of STDIO (Unix only), SSH, IOM (Remote and Local) and HTTP, and partially supports connection method of COM. You can visit the following [link](https://sassoftware.github.io/saspy) to find out how to install SASPy and how to configure it. + +### Profile Anatomy + +The parameters listed below make up the profile settings for configuring a connection to a Python/SASPy for SAS 9.4 instance. + +| Name | Description | Additional Notes | +| -------------- | -------------- | ----------------------------------------------------------------------------- | +| **pythonpath** | Path to python | Defaults to `python` | +| **cfgname** | sascfg name | Visit [link](sassoftware.github.io/saspy/configuration.html) for introduction | + +### Add New SAS 9.4 (SASPy) Profile + +Open the command palette (`F1`, or `Ctrl+Shift+P` on Windows). After executing the `SAS.addProfile` command, select the SAS 9.4 (SASpy) connection type to create a new profile. + ## Additional settings in a profile -| Name | Supported Connection Types | Description | Additional Notes | -| ------------------------ | ----------------------------------------------------------------------------- | ------------------------------------------- | ------------------------------------------------ | -| **SAS Options Settings** | SAS Viya, SAS 9.4 (local)\*, SAS 9.4 (remote - IOM)\*, SAS 9.4 (remote - SSH) | SAS options to apply to the SAS session | \* local startup options currently not supported | -| **AutoExec Settings** | SAS Viya | SAS code to execute once at session startup | | +| Name | Supported Connection Types | Description | Additional Notes | +| ------------------------ | ---------------------------------------------------------------------------------------------- | ------------------------------------------- | ------------------------------------------------ | +| **SAS Options Settings** | SAS Viya, SAS 9.4 (local)\*, SAS 9.4 (remote - IOM)\*, SAS 9.4 (remote - SSH), SAS 9.4 (SASPy) | SAS options to apply to the SAS session | \* local startup options currently not supported | +| **AutoExec Settings** | SAS Viya | SAS code to execute once at session startup | | ### SAS Options Settings @@ -260,6 +277,20 @@ SAS System Options can be set per connection profile. Changes made to SAS Option } ``` +- SAS 9.4 (SASPy): + + ```json + { + "profiles": { + "sas9SASPY": { + "cfgname": "winlocal", + "sasOptions": ["NONEWS", "ECHOAUTO", "PAGESIZE=MAX"], + "ConnectionType": "saspy" + } + } + } + ``` + ### AutoExec Settings For Viya connection profiles, it's possible to setup AutoExec lines that will execute once per session startup. Changes made to the autoexec require closing and reopening the session to take effect. The AutoExec option supports different modes for how to define the SAS lines that should run: