1
0
mirror of https://github.com/sasjs/core.git synced 2025-12-11 06:24:35 +00:00

Compare commits

...

87 Commits

Author SHA1 Message Date
Allan Bowe
bbbc28ad6d Merge branch 'main' of github.com:sasjs/core 2021-05-15 16:31:13 +03:00
Allan Bowe
530cd6e95c feat: adding two more test types to mp_assertdsobs.sas, also a test for the assertion macro itself 2021-05-15 16:30:57 +03:00
Allan Bowe
c4e17e43e8 Create CODE_OF_CONDUCT.md 2021-05-15 15:03:59 +03:00
Allan Bowe
fed217eec3 Merge pull request #29 from sasjs/issue10
fix: adding checks for consul token access, and two tests to ensure t…
2021-05-15 14:42:53 +03:00
Allan Bowe
1934dc8332 fix: adding checks for consul token access, and two tests to ensure the macro is working. closes #10 2021-05-15 14:41:10 +03:00
Allan Bowe
9de512cfc7 Merge pull request #28 from sasjs/issue7
fix: adding licence info.  Closes #7
2021-05-13 21:50:36 +03:00
Allan Bowe
cadafcc86b fix: adding licence info. Closes #7 2021-05-13 21:49:54 +03:00
Allan Bowe
5f805b006f Merge pull request #27 from sasjs/issue14
feat: adding MATCH parameter to mp_searchcols.sas to enable fuzzy mat…
2021-05-13 21:40:03 +03:00
Allan Bowe
c6b65366b7 feat: adding MATCH parameter to mp_searchcols.sas to enable fuzzy matching on columns. Closes #14 2021-05-13 21:38:38 +03:00
Allan Bowe
51ddd9c1e5 chore: automated commit 2021-05-13 10:38:02 +03:00
Allan Bowe
20bf3b86af chore: automated commit 2021-05-13 10:34:54 +03:00
Allan Bowe
de67cd329b chore: automated commit 2021-05-12 16:32:24 +03:00
Allan Bowe
779e4942c7 Merge pull request #26 from tmoody/fix/clean_exit_mv_jobflow_on_syscc
fix: early exit, with syscc, when submitted jobs fail within a flow
2021-05-12 16:31:06 +03:00
Trevor Moody
a69a1ac7f0 fix: removed invisible hexchars on blank lines 2021-05-12 14:18:44 +01:00
Trevor Moody
2a644d6c2b fix: corrected asser description 2021-05-12 14:01:49 +01:00
Trevor Moody
843930c666 chore: added tests for mv_jobflow 2021-05-12 13:59:21 +01:00
Trevor Moody
90d69af7ee feat: early exit, with syscc, when submitted jobs fail within a flow 2021-05-12 12:06:02 +01:00
Allan Bowe
b7bafb49f4 Merge pull request #25 from sasjs/dcfixes
fix: more logging in mp_abort, fixing job test, better return values …
2021-05-11 23:37:25 +03:00
Allan Bowe
2fa9e48286 chore: automated commit 2021-05-11 23:36:40 +03:00
Allan Bowe
5cee93c7bd fix: more logging in mp_abort, fixing job test, better return values in mp_filtervalidate and mp_filtercheck, further fixes in mp_jsonout 2021-05-11 23:08:54 +03:00
Allan Bowe
1a595c64c6 Merge pull request #24 from sasjs/abortfix
fix: mp_abort cleanup
2021-05-11 21:14:46 +03:00
Allan Bowe
2c901831b7 chore: automated commit 2021-05-11 21:11:24 +03:00
Allan Bowe
28209950ab chore: automated commit 2021-05-11 20:49:57 +03:00
Allan Bowe
44069e9867 chore: automated commit 2021-05-11 20:32:12 +03:00
Allan Bowe
e26af5c09a chore: automated commit 2021-05-11 20:32:00 +03:00
Allan Bowe
4ee13c9389 fix: 400 log repeat, refactor mp_abort abortions, updated doc header 2021-05-11 20:25:39 +03:00
Allan Bowe
15f903aa42 fix: updating mv_getjoblog to deal with endsas'd sessions, and removing endsas from viya mp_abort 2021-05-11 18:14:33 +03:00
Allan Bowe
58a0cce39e chore: automated commit 2021-05-11 13:36:25 +03:00
Allan Bowe
9a5574ea0e fix: all 2021-05-11 13:35:21 +03:00
Allan Bowe
e6146dcbcf fix: more logic to improve robustness 2021-05-11 12:39:10 +03:00
Allan Bowe
583c7e0c83 fix: removing string 2021-05-11 12:14:09 +03:00
Allan Bowe
223bdd5983 fix: mp_abort cleanup 2021-05-11 11:58:27 +03:00
Allan Bowe
aef14543f0 chore: docs update 2021-05-10 20:37:02 +03:00
Allan Bowe
c88764c1d8 chore: docs update 2021-05-10 20:15:51 +03:00
Allan Bowe
2c952c8b01 chore: docs update 2021-05-10 20:14:23 +03:00
Allan Bowe
de3610d1aa Merge branch 'main' of github.com:sasjs/core 2021-05-10 15:24:23 +03:00
Allan Bowe
d35d597437 fix: updating reason_cd in mp_filtercheck.sas 2021-05-10 15:24:13 +03:00
Allan Bowe
3e8deda008 fix: description for mf_abort 2021-05-10 15:23:30 +03:00
Allan Bowe
a27496c7b3 Merge pull request #23 from tmoody/fix/non_stp_syscc_propagation
fix: Non-stp propagation of syscc
2021-05-10 14:58:04 +03:00
Allan Bowe
265389befc Merge branch 'main' into fix/non_stp_syscc_propagation 2021-05-10 14:57:24 +03:00
Trevor Moody
db2531e0b3 fix: Non-stp propagation of syscc 2021-05-10 12:42:36 +01:00
Allan Bowe
5e77494aa6 Merge pull request #22 from sasjs/mp_json
fix: adding formatting to mp_jsonout, and a test
2021-05-10 03:44:08 +03:00
Allan Bowe
6a2ac51925 fix: adding formatting to mp_jsonout, and a test 2021-05-10 03:42:53 +03:00
Allan Bowe
f625b04189 Merge pull request #21 from sasjs/ds2fmtds
feat: new mp_ds2fmtds macro - converts a dataset to a new dataset whe…
2021-05-10 01:26:51 +03:00
Allan Bowe
68aee776d3 feat: new mp_ds2fmtds macro - converts a dataset to a new dataset where all values are the formatted values. Also added a test. 2021-05-10 01:25:51 +03:00
Allan Bowe
38d2195d32 Merge pull request #20 from sasjs/testframework
fix: refreshed the testing toolkit, added debug options, updated docu…
2021-05-08 23:29:21 +03:00
Allan Bowe
4e564b5409 fix: refreshed the testing toolkit, added debug options, updated documentation, included one new test (mv_getjobcode.sas) 2021-05-08 23:27:55 +03:00
Allan Bowe
298acc4e50 fix: setting default to best. over 8. for mf_getformat with force option 2021-05-07 11:20:01 +03:00
Allan Bowe
af98909753 Merge pull request #19 from sasjs/mp_assert
feat: new (generic) mp_assert macro, and new feature (type filter) fo…
2021-05-06 20:59:49 +03:00
Allan Bowe
b9d33b38bf feat: new (generic) mp_assert macro, and new feature (type filter) for mf_getvarlist. Added/updated tests for mp_filtercheck and mp_validatecol and mf_getvarlist. 2021-05-06 20:58:38 +03:00
Allan Bowe
b61b5f1856 fix: adding dependency 2021-05-06 19:05:14 +03:00
Allan Bowe
805474bb46 Merge pull request #18 from sasjs/jobresult
fix: enabling fileref as output option for sas code obtained via mm_g…
2021-05-06 15:27:18 +03:00
Allan Bowe
61701f3c6a fix: enabling fileref as output option for sas code obtained via mm_getstpcode. Also updated some doc headers and macro footers. 2021-05-06 15:06:13 +03:00
Allan Bowe
f20d7476bf Merge pull request #17 from sasjs/jobresult
feat: new mv_getjobresult.sas macro, corresponding test, and additional fixes
2021-05-06 01:08:56 +03:00
Allan Bowe
04a3189a89 feat: new mv_getjobresult.sas macro, corresponding test, and additional fixes 2021-05-06 01:07:25 +03:00
Allan Bowe
b1380983ec fix: missing comma 2021-05-05 20:16:26 +03:00
Allan Bowe
b4834f9b40 fix: updates following test runs in Studio 2021-05-05 20:12:06 +03:00
Allan Bowe
1b5ad93cad chore: updating all.sas 2021-05-05 11:48:38 +03:00
Allan Bowe
f2942f2032 chore: adding sasjsresults to .gitignore 2021-05-05 01:39:53 +03:00
Allan Bowe
4198448b81 chore: removing temp results folder 2021-05-05 01:39:29 +03:00
Allan Bowe
47a33452e0 Merge pull request #16 from sasjs/testrelease
feat: new macro for validating inputs (mp_validatecol.sas), also a re…
2021-05-05 01:36:38 +03:00
Allan Bowe
fb21a0adfd feat: new macro for validating inputs (mp_validatecol.sas), also a refresh of the tests now that sasjs test is released. All tests are passing 2021-05-05 01:35:00 +03:00
Allan Bowe
e01b06b640 feat: new assertion macro (mp_assertcols.sas) to test for column existence (or not) 2021-05-04 21:53:57 +03:00
Allan Bowe
24380ddf26 Merge branch 'main' of github.com:sasjs/core 2021-05-03 22:44:07 +03:00
Allan Bowe
1ef42d45af fix: wrapping filter query in brackets to allow logic to be encapsulated when using with other logic sources 2021-05-03 22:43:56 +03:00
Allan Bowe
6ee13a2779 Merge pull request #15 from sasjs/removewarns
fix: removing WARNINGs from code logic
2021-05-03 20:30:43 +03:00
Allan Bowe
ffd2e135dc fix: removing WARNINGs from code logic 2021-05-03 20:28:48 +03:00
Allan Bowe
7f2ad5fc66 feat: mp_filtervalidate.sas - to run a proc sql validate against the target table to ensure validity 2021-05-03 13:48:24 +03:00
ff1eb54cc3 chore: updating logo 2021-05-03 10:59:25 +02:00
Allan Bowe
d6235c6357 chore: reducing raw_value size to 4000 for wider DB support 2021-05-03 11:17:20 +03:00
Allan Bowe
98118adb9a feat: new assertion macro for testing the values in a target column. Designed for use with 'sasjs test'. 2021-05-03 01:00:36 +03:00
Allan Bowe
369c4412f3 fix: adding tests for mp_filtergenerate, also updating the corresponding macros following test results 2021-05-02 22:11:44 +03:00
Allan Bowe
7d7608f06c chore: updating all.sas 2021-05-02 19:12:08 +03:00
Allan Bowe
3791cb8a2c feat: two new macros for checking a filter query, and then generating a filter query. One test to cover the generation part. One more macro to provide assertions on the number of rows in a table, compatible with the upcoming 'sasjs test' feature. 2021-05-02 19:10:37 +03:00
Allan Bowe
ff82f7d75c chore: header update in mf_getvarlist 2021-05-01 20:33:45 +03:00
Allan Bowe
fdd566e8ce fix: setting server headers only if STREAM mode enabled to avoid 'Function is only valid for filerefs using the CACHE access method.' error when testing STPs from Studio. Also removing proc json as it cannot handle invalid characters. 2021-05-01 16:27:41 +03:00
Beast
328f8c260b chore: updating all.sas 2021-05-01 15:34:30 +03:00
Beast
029169ac80 feat: adding mf_getxengine macro for determining the engine of a sas fileref 2021-05-01 15:32:45 +03:00
66ff1de7a9 fix: reducing logging 2021-04-23 23:06:37 +02:00
053290c7df chore: updating header of mp_hashdataset 2021-04-23 07:56:33 +02:00
af71a5e53b feat: new macro for hashing a table (mp_hashdataset) 2021-04-23 00:33:11 +02:00
ecdce86287 fix: adding all.sas also 2021-04-17 00:11:33 +02:00
ba1272aaf7 fix: updating mv_createwebservice to support 0x01 hex characters, adding a test (and test scaffolding) as part of this. The test scaffolding will be updated once goes live - for now it is being deployed as a service. 2021-04-17 00:11:06 +02:00
d6056b9397 fix: adding mod statement to _webout to enable sas-side sasjs testing 2021-04-10 12:41:17 +02:00
Allan Bowe
00511c72c2 fix: removing deprecated params from mm_createstp 2021-04-09 11:46:37 +00:00
Allan Bowe
1d6f04fd56 chore: adding macro related lint settings and sasjs as a recommended extension 2021-04-08 15:59:08 +00:00
af4dbb5632 feat: switching to DATASTEP over PROCJSON for json delivery in sasjs/adapter 2021-04-08 09:49:50 +02:00
90 changed files with 6535 additions and 891 deletions

6
.gitignore vendored
View File

@@ -1,9 +1,13 @@
node_modules
.DS_Store
sasjsbuild/
sasjsresults/
# avoid filenames with spaces being committed to source control
**\ **
# ignore the mc_* files - containing macros for individual libraries
mc_*
mc_*
# ignore .env files as they can contain sasjs access tokens
*.env*

View File

@@ -5,4 +5,4 @@ image:
file: .gitpod.dockerfile
vscode:
extensions:
- sasjs.sasjs-for-vscode@1.7.2:R6y1nzpFh2P99BZg5FgH5g==
- sasjs.sasjs-for-vscode

View File

@@ -2,6 +2,9 @@
"noTrailingSpaces": true,
"noEncodedPasswords": true,
"hasDoxygenHeader": true,
"hasMacroNameInMend": false,
"hasMacroParentheses": true,
"noNestedMacros": false,
"noSpacesInFileNames": true,
"maxLineLength": 135,
"lowerCaseFileNames": true,

5
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,5 @@
{
"recommendations": [
"sasjs.sasjs-for-vscode"
]
}

128
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
https://sasapps.io/contact-us.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

View File

@@ -116,19 +116,19 @@ The **Macro Core** documentation is created using [doxygen](http://www.doxygen.n
All macros must be commented in the doxygen format, to enable the [online documentation](https://core.sasjs.io).
### Dependencies
SAS code can contain one of two types of dependency - SAS Macros, and SAS Programs. When compiling projects using the [SASjs CLI](https://cli.sasjs.io) the doxygen header is scanned for ` @li` items under the following headers:
SAS code can contain one of two types of dependency - SAS Macros, and SAS Includes. When compiling projects using the [SASjs CLI](https://cli.sasjs.io) the doxygen header is scanned for ` @li` items under the following headers:
```sas
<h4> SAS Macros </h4>
@li mf_nobs.sas
@li mm_assignlib.sas
<h4> SAS Programs </h4>
<h4> SAS Includes </h4>
@li somefile.ddl SOMEFREF
@li someprogram.sas FREFTWO
```
The CLI can then extract all the dependencies and insert as precode (SAS Macros) or in a temp engine fileref (SAS Programs) when creating SAS Jobs and Services.
The CLI can then extract all the dependencies and insert as precode (SAS Macros) or in a temp engine fileref (SAS Includes) when creating SAS Jobs and Services.
When contributing to this library, it is therefore important to ensure that all dependencies are listed in the header in this format.

3031
all.sas

File diff suppressed because it is too large Load Diff

View File

@@ -1,23 +1,11 @@
/**
@file
@brief abort gracefully according to context
@details Do not use directly! See bottom of explanation for details.
@brief to be deprecated
@details We will deprecate this macro in 2022
Configures an abort mechanism according to site specific policies or the
particulars of an environment. For instance, can stream custom
results back to the client in an STP Web App context, or completely stop
in the case of a batch run.
As you can see, it's not a macro function.
For the sharp eyed readers - this is no longer a macro function!! It became
a macro procedure during a project and now it's kinda stuck that way until
that project is updated (if it's ever updated). In the meantime we created
`mp_abort` which is just a wrapper for this one, and so we recomend you use
that for forwards compatibility reasons.
@param mac= to contain the name of the calling macro
@param type= deprecated. Not used.
@param msg= message to be returned
@param iftrue= supply a condition under which the macro should be executed.
Use mp_abort.sas instead.
@version 9.2
@author Allan Bowe

View File

@@ -2,9 +2,9 @@
@file
@brief Checks if a set of variables ALL exist in a data set.
@details Returns 0 if ANY of the variables do not exist, or 1 if they ALL do.
Usage:
Usage:
%put %mf_existVarList(sashelp.class, age sex name dummyvar)
%put %mf_existVarList(sashelp.class, age sex name dummyvar);
<h4> SAS Macros </h4>
@li mf_abort.sas
@@ -29,7 +29,7 @@
%let dsid=%sysfunc(open(&libds,is));
%if &dsid=0 %then %do;
%put WARNING: unable to open &libds in mf_existvarlist (&dsid);
%put %str(WARN)ING: unable to open &libds in mf_existvarlist (&dsid);
%end;
%if %sysfunc(attrn(&dsid,NVARS))=0 %then %do;

View File

@@ -23,7 +23,7 @@
%local dsid rc;
%let dsid=%sysfunc(open(&libds,is));
%if &dsid = 0 %then %do;
%put WARNING: Cannot open %trim(&libds), system message below;
%put %str(WARN)ING: Cannot open %trim(&libds), system message below;
%put %sysfunc(sysmsg());
-1
%end;

View File

@@ -22,6 +22,9 @@
@version 9.2
@author Allan Bowe
<h4> Related Macros </h4>
@li mf_getxengine.sas
**/
/** @cond */

View File

@@ -18,5 +18,5 @@
%macro mf_getuniquename(prefix=MC);
&prefix.%substr(%sysfunc(compress(%sysfunc(uuidgen()),-)),1,32-%length(&prefix))
%mend;
&prefix.%substr(%sysfunc(compress(%sysfunc(uuidgen()),-)),1,32-%length(&prefix))
%mend mf_getuniquename;

View File

@@ -23,9 +23,9 @@
8.
NOTE: Variable renegade does not exist in test
@param libds Two part dataset (or view) reference.
@param var Variable name for which a format should be returned
@param force Set to 1 to supply a default if the variable has no format
@param [in] libds Two part dataset (or view) reference.
@param [in] var Variable name for which a format should be returned
@param [in] force=(0) Set to 1 to supply a default if the variable has no format
@returns outputs format
@author Allan Bowe
@@ -60,7 +60,7 @@
%let vlen = %sysfunc(varlen(&dsid, &vnum));
%let vtype = %sysfunc(vartype(&dsid, &vnum.));
%if &vtype=C %then %let vformat=$&vlen..;
%else %let vformat=8.;
%else %let vformat=best.;
%end;
@@ -68,4 +68,4 @@
%let rc = %sysfunc(close(&dsid));
/* Return variable format */
&vformat
%mend;
%mend mf_getVarFormat;

View File

@@ -10,14 +10,21 @@
returns:
> List of Variables=Name Sex Age Height Weight
For a seperated list of column values:
%put %mf_getvarlist(sashelp.class,dlm=%str(,),quote=double);
returns:
> "Name","Sex","Age","Height","Weight"
@param libds Two part dataset (or view) reference.
@param dlm= provide a delimiter (eg comma or space) to separate the vars
@param quote= use either DOUBLE or SINGLE to quote the results
@param [in] libds Two part dataset (or view) reference.
@param [in] dlm= ( ) Provide a delimiter (eg comma or space) to separate the
variables
@param [in] quote= (none) use either DOUBLE or SINGLE to quote the results
@param [in] typefilter= (A) Filter for certain types of column. Valid values:
@li A Return All columns
@li C Return Character columns
@li N Return Numeric columns
@version 9.2
@author Allan Bowe
@@ -27,9 +34,10 @@
%macro mf_getvarlist(libds
,dlm=%str( )
,quote=no
,typefilter=A
)/*/STORE SOURCE*/;
/* declare local vars */
%local outvar dsid nvars x rc dlm q var;
%local outvar dsid nvars x rc dlm q var vtype;
/* credit Rowland Hale - byte34 is double quote, 39 is single quote */
%if %upcase(&quote)=DOUBLE %then %let q=%qsysfunc(byte(34));
@@ -37,21 +45,22 @@
/* open dataset in macro */
%let dsid=%sysfunc(open(&libds));
%if &dsid %then %do;
%let nvars=%sysfunc(attrn(&dsid,NVARS));
%if &nvars>0 %then %do;
/* add first dataset variable to global macro variable */
%let outvar=&q.%sysfunc(varname(&dsid,1))&q.;
/* add remaining variables with supplied delimeter */
/* add variables with supplied delimeter */
%do x=1 %to &nvars;
%let var=&q.%sysfunc(varname(&dsid,&x))&q.;
%if &var=&q&q %then %do;
%put &sysmacroname: Empty column found in &libds!;
%let var=&q. &q.;
/* get variable type */
%let vtype=%sysfunc(vartype(&dsid,&x));
%if &vtype=&typefilter or &typefilter=A %then %do;
%let var=&q.%sysfunc(varname(&dsid,&x))&q.;
%if &var=&q&q %then %do;
%put &sysmacroname: Empty column found in &libds!;
%let var=&q. &q.;
%end;
%if %quote(&outvar)=%quote() %then %let outvar=&var;
%else %let outvar=&outvar.&dlm.&var.;
%end;
%if &x=1 %then %let outvar=&var;
%else %let outvar=&outvar.&dlm.&var.;
%end;
%end;
%let rc=%sysfunc(close(&dsid));
@@ -61,4 +70,4 @@
%let rc=%sysfunc(close(&dsid));
%end;
&outvar
%mend;
%mend mf_getvarlist;

View File

@@ -45,4 +45,4 @@ Usage:
%let rc = %sysfunc(close(&dsid));
/* Return variable type */
&vtype
%mend;
%mend mf_getvartype;

43
base/mf_getxengine.sas Normal file
View File

@@ -0,0 +1,43 @@
/**
@file
@brief Returns the engine type of a SAS fileref
@details Queries sashelp.vextfl to get the xengine value.
Usage:
filename feng temp;
%put %mf_getxengine(feng);
returns:
> TEMP
@param fref The fileref to check
@returns The XENGINE value in sashelp.vextfl or 0 if not found.
@version 9.2
@author Allan Bowe
<h4> Related Macros </h4>
@li mf_getengine.sas
**/
%macro mf_getxengine(fref
)/*/STORE SOURCE*/;
%local dsid engnum rc engine;
%let dsid=%sysfunc(
open(sashelp.vextfl(where=(fileref="%upcase(&fref)")),i)
);
%if (&dsid ^= 0) %then %do;
%let engnum=%sysfunc(varnum(&dsid,XENGINE));
%let rc=%sysfunc(fetch(&dsid));
%let engine=%sysfunc(getvarc(&dsid,&engnum));
%* put &fref. ENGINE is &engine.;
%let rc= %sysfunc(close(&dsid));
%end;
%else %let engine=0;
&engine
%mend;

View File

@@ -18,4 +18,4 @@
&today._&now._&sysjobid._%sysevalf(%sysfunc(ranuni(0))*999,CEIL)
%mend;
%mend mf_uid;

View File

@@ -30,7 +30,7 @@
%local count_base count_extr i i2 extr_word base_word match outvar;
%if %length(&str1)=0 or %length(&str2)=0 %then %do;
%put WARNING: empty string provided!;
%put %str(WARN)ING: empty string provided!;
%put base string (str1)= &str1;
%put compare string (str2) = &str2;
%return;

View File

@@ -4,13 +4,24 @@
@details Configures an abort mechanism according to site specific policies or
the particulars of an environment. For instance, can stream custom
results back to the client in an STP Web App context, or completely stop
in the case of a batch run.
in the case of a batch run. For STP sessions
The method used varies according to the context. Important points:
@li should not use endsas or abort cancel in 9.4m3 environments as this can
cause hung multibridge sessions and result in a frozen STP server
@li should not use endsas in viya 3.5 as this destroys the session and cannot
fetch results (although both mv_getjoblog.sas and the @sasjs/adapter will
recognise this and fetch the log of the parent session instead)
@li STP environments must finish cleanly to avoid the log being sent to
_webout. To assist with this, we also run stpsrvset('program error', 0)
and set SYSCC=0. For 9.4m3 we take a unique approach - we open a macro
but don't close it! This provides a graceful abort, EXCEPT when called
called within a %include within a macro (and that macro contains additional
logic). See mp_abort.test.nofix.sas for the example case.
If you know of another way to gracefully abort a 9.4m3 STP session, we'd
love to hear about it!
Using SAS Abort Cancel mechanisms can cause hung sessions in some Stored
Process environments. This macro takes a unique approach - we set the SAS
syscc to 0, run `stpsrvset('program error', 0)` (if SAS 9) and then - we open
a macro but don't close it! This provides a graceful abort for SAS web
services in all web enabled environments.
@param mac= to contain the name of the calling macro
@param msg= message to be returned
@@ -24,6 +35,8 @@
%macro mp_abort(mac=mp_abort.sas, type=, msg=, iftrue=%str(1=1)
)/*/STORE SOURCE*/;
%global sysprocessmode sysprocessname;
%if not(%eval(%unquote(&iftrue))) %then %return;
%put NOTE: /// mp_abort macro executing //;
@@ -31,9 +44,7 @@
%put NOTE - &msg;
/* Stored Process Server web app context */
%if %symexist(_metaperson)
or (%symexist(SYSPROCESSNAME) and "&SYSPROCESSNAME"="Compute Server" )
%then %do;
%if %symexist(_metaperson) or "&SYSPROCESSNAME "="Compute Server " %then %do;
options obs=max replace nosyntaxcheck mprint;
/* extract log errs / warns, if exist */
%local logloc logline;
@@ -63,7 +74,7 @@
input;
i=1;
stoploop=0;
if _n_ ge &logline-5 and stoploop=0 then do until (i>12);
if _n_ ge &logline-15 and stoploop=0 then do until (i>22);
call symputx('logmsg',catx('\n',symget('logmsg'),_infile_));
input;
i+1;
@@ -128,31 +139,59 @@
if debug ge '"131"' then put '>>weboutEND<<';
run;
%let syscc=0;
%if %symexist(_metaport) %then %do;
%put _all_;
%if "&sysprocessmode " = "SAS Stored Process Server " %then %do;
data _null_;
if symexist('sysprocessmode')
then if symget("sysprocessmode")="SAS Stored Process Server"
then rc=stpsrvset('program error', 0);
putlog 'stpsrvset program error and syscc';
rc=stpsrvset('program error', 0);
call symputx("syscc",0,"g");
run;
%if "%substr(&sysvlong.xxxxxxxxx,1,9)" ne "9.04.01M3" %then %do;
%put NOTE: Ending SAS session due to:;
%put NOTE- &msg;
endsas;
%end;
%end;
%else %if "&sysprocessmode " = "SAS Compute Server " %then %do;
/* endsas kills the session making it harder to fetch results */
data _null_;
syswarningtext=symget('syswarningtext');
syserrortext=symget('syserrortext');
abort_msg=symget('msg');
syscc=symget('syscc');
sysuserid=symget('sysuserid');
iftrue=symget('iftrue');
put (_all_)(/=);
abort cancel nolist;
run;
%end;
/**
* endsas is reliable but kills some deployments.
* Abort variants are ungraceful (non zero return code)
* This approach lets SAS run silently until the end :-)
*/
%put _all_;
filename skip temp;
data _null_;
file skip;
put '%macro skip(); %macro skippy();';
run;
%inc skip;
%else %if "%substr(&sysvlong.xxxxxxxxx,1,9)" = "9.04.01M3" %then %do;
/**
* endsas kills 9.4m3 deployments by orphaning multibridges.
* Abort variants are ungraceful (non zero return code)
* This approach lets SAS run silently until the end :-)
* Caution - fails when called within a %include within a macro
* See tests/mp_abort.test.1 for an example case.
*/
filename skip temp;
data _null_;
file skip;
put '%macro skip();';
comment '%mend skip; -> fix lint ';
put '%macro skippy();';
comment '%mend skippy; -> fix lint ';
run;
%inc skip;
%end;
%else %do;
%abort cancel;
%end;
%end;
%else %do;
%put _all_;
%abort cancel;
%end;
%mend;
%mend mp_abort;
/** @endcond */

56
base/mp_assert.sas Normal file
View File

@@ -0,0 +1,56 @@
/**
@file
@brief Generic assertion
@details Useful in the context of writing sasjs tests. The results of the
test are _appended_ to the &outds. table.
Example usage:
%mp_assert(iftrue=(1=1),
desc=Obviously true
)
%mp_assert(iftrue=(1=0),
desc=Will fail
)
@param [in] iftrue= (1=1) A condition where, if true, the test is a PASS.
Else, the test is a fail.
@param [in] desc= (Testing observations) The user provided test description
@param [out] outds= (work.test_results) The output dataset to contain the
results. If it does not exist, it will be created, with the following format:
|TEST_DESCRIPTION:$256|TEST_RESULT:$4|TEST_COMMENTS:$256|
|---|---|---|
|User Provided description|PASS|Column &inds contained ALL columns|
@version 9.2
@author Allan Bowe
**/
%macro mp_assert(iftrue=(1=1),
desc=0,
outds=work.test_results
)/*/STORE SOURCE*/;
data ;
length test_description $256 test_result $4 test_comments $256;
test_description=symget('desc');
test_comments="&sysmacroname: Test result of "!!symget('iftrue');
%if %eval(%unquote(&iftrue)) %then %do;
test_result='PASS';
%end;
%else %do;
test_result='FAIL';
%end;
run;
%local ds ;
%let ds=&syslast;
proc append base=&outds data=&ds;
run;
proc sql;
drop table &ds;
%mend mp_assert;

145
base/mp_assertcols.sas Normal file
View File

@@ -0,0 +1,145 @@
/**
@file
@brief Asserts the existence (or not) of columns
@details Useful in the context of writing sasjs tests. The results of the
test are _appended_ to the &outds. table.
Example usage:
%mp_assertcols(sashelp.class,
cols=name age sex,
test=ALL,
desc=check all columns exist
)
%mp_assertcols(sashelp.class,
cols=a b c,
test=NONE
)
%mp_assertcols(sashelp.class,
cols=age depth,
test=ANY
)
<h4> SAS Macros </h4>
@li mf_existds.sas
@li mf_existvarlist.sas
@li mf_getvarlist.sas
@li mf_wordsinstr1butnotstr2.sas
@li mp_abort.sas
@param [in] inds The input library.dataset to test for values
@param [in] cols= The list of columns to check for
@param [in] desc= (Testing observations) The user provided test description
@param [in] test= (ALL) The test to apply. Valid values are:
@li ALL - Test is a PASS if ALL columns exist in &inds
@li ANY - Test is a PASS if ANY of the columns exist in &inds
@li NONE - Test is a PASS if NONE of the columns exist in &inds
@param [out] outds= (work.test_results) The output dataset to contain the
results. If it does not exist, it will be created, with the following format:
|TEST_DESCRIPTION:$256|TEST_RESULT:$4|TEST_COMMENTS:$256|
|---|---|---|
|User Provided description|PASS|Column &inds contained ALL columns|
<h4> Related Macros </h4>
@li mp_assertdsobs.sas
@li mp_assertcolvals.sas
@li mp_assertdsobs.sas
@version 9.2
@author Allan Bowe
**/
%macro mp_assertcols(inds,
cols=0,
test=ALL,
desc=0,
outds=work.test_results
)/*/STORE SOURCE*/;
%mp_abort(iftrue= (&syscc ne 0)
,mac=&sysmacroname
,msg=%str(syscc=&syscc - on macro entry)
)
%local lib ds ;
%let lib=%scan(&inds,1,%str(.));
%let ds=%scan(&inds,2,%str(.));
%let cols=%upcase(&cols);
%mp_abort(iftrue= (%mf_existds(&lib..&ds)=0)
,mac=&sysmacroname
,msg=%str(&lib..&ds not found!)
)
%mp_abort(iftrue= (&cols=0)
,mac=&sysmacroname
,msg=%str(No cols provided)
)
%let test=%upcase(&test);
%if &test ne ANY and &test ne ALL and &test ne NONE %then %do;
%mp_abort(
mac=&sysmacroname,
msg=%str(Invalid test - &test)
)
%end;
/**
* now do the actual test!
*/
%local result;
%if %mf_existVarList(&inds,&cols)=1 %then %let result=ALL;
%else %do;
%local targetcols compare;
%let targetcols=%upcase(%mf_getvarlist(&inds));
%let compare=%mf_wordsinstr1butnotstr2(
Str1=&cols,
Str2=&targetcols
);
%if %cmpres(&compare)=%cmpres(&cols) %then %let result=NONE;
%else %let result=SOME;
%end;
data;
length test_description $256 test_result $4 test_comments $256;
test_description=symget('desc');
if test_description='0'
then test_description="Testing &inds for existence of &test of: &cols";
test_result='FAIL';
test_comments="&sysmacroname: &inds has &result columns ";
%if &test=ALL %then %do;
%if &result=ALL %then %do;
test_result='PASS';
%end;
%end;
%else %if &test=ANY %then %do;
%if &result=SOME %then %do;
test_result='PASS';
%end;
%end;
%else %if &test=NONE %then %do;
%if &result=NONE %then %do;
test_result='PASS';
%end;
%end;
%else %do;
test_comments="&sysmacroname: Unsatisfied test condition - &test";
%end;
run;
%local ds;
%let ds=&syslast;
proc append base=&outds data=&ds;
run;
proc sql;
drop table &ds;
%mend;

147
base/mp_assertcolvals.sas Normal file
View File

@@ -0,0 +1,147 @@
/**
@file
@brief Asserts the values in a column
@details Useful in the context of writing sasjs tests. The results of the
test are _appended_ to the &outds. table.
Example usage:
data work.checkds;
do checkval='Jane','James','Jill';
output;
end;
run;
%mp_assertcolvals(sashelp.class.name,
checkvals=work.checkds.checkval,
desc=At least one value has a match,
test=ANYVAL
)
data work.check;
do val='M','F';
output;
end;
run;
%mp_assertcolvals(sashelp.class.sex,
checkvals=work.check.val,
desc=All values have a match,
test=ALLVALS
)
<h4> SAS Macros </h4>
@li mf_existds.sas
@li mf_nobs.sas
@li mp_abort.sas
@param [in] indscol The input library.dataset.column to test for values
@param [in] checkvals= A library.dataset.column value containing a UNIQUE
list of values to be compared against the source (indscol).
@param [in] desc= (Testing observations) The user provided test description
@param [in] test= (ALLVALS) The test to apply. Valid values are:
@li ALLVALS - Test is a PASS if ALL values have a match in checkvals
@li ANYVAL - Test is a PASS if at least 1 value has a match in checkvals
@param [out] outds= (work.test_results) The output dataset to contain the
results. If it does not exist, it will be created, with the following format:
|TEST_DESCRIPTION:$256|TEST_RESULT:$4|TEST_COMMENTS:$256|
|---|---|---|
|User Provided description|PASS|Column &indscol contained ALL target vals|
<h4> Related Macros </h4>
@li mp_assertdsobs.sas
@version 9.2
@author Allan Bowe
**/
%macro mp_assertcolvals(indscol,
checkvals=0,
test=ALLVALS,
desc=mp_assertcolvals - no desc provided,
outds=work.test_results
)/*/STORE SOURCE*/;
%mp_abort(iftrue= (&syscc ne 0)
,mac=&sysmacroname
,msg=%str(syscc=&syscc - on macro entry)
)
%local lib ds col clib cds ccol nobs;
%let lib=%scan(&indscol,1,%str(.));
%let ds=%scan(&indscol,2,%str(.));
%let col=%scan(&indscol,3,%str(.));
%mp_abort(iftrue= (%mf_existds(&lib..&ds)=0)
,mac=&sysmacroname
,msg=%str(&lib..&ds not found!)
)
%mp_abort(iftrue= (&checkvals=0)
,mac=&sysmacroname
,msg=%str(Set CHECKVALS to a library.dataset.column containing check vals)
)
%let clib=%scan(&checkvals,1,%str(.));
%let cds=%scan(&checkvals,2,%str(.));
%let ccol=%scan(&checkvals,3,%str(.));
%mp_abort(iftrue= (%mf_existds(&clib..&cds)=0)
,mac=&sysmacroname
,msg=%str(&clib..&cds not found!)
)
%let nobs=%mf_nobs(&clib..&cds);
%mp_abort(iftrue= (&nobs=0)
,mac=&sysmacroname
,msg=%str(&clib..&cds is empty!)
)
%let test=%upcase(&test);
%if &test ne ALLVALS and &test ne ANYVAL %then %do;
%mp_abort(
mac=&sysmacroname,
msg=%str(Invalid test - &test)
)
%end;
%local result orig;
%let result=-1;
%let orig=-1;
proc sql noprint;
select count(*) into: result
from &lib..&ds
where &col not in (
select &ccol from &clib..&cds
);
select count(*) into: orig from &lib..&ds;
quit;
%mp_abort(iftrue= (&syscc ne 0)
,mac=&sysmacroname
,msg=%str(syscc=&syscc after macro query)
)
data;
length test_description $256 test_result $4 test_comments $256;
test_description=symget('desc');
test_result='FAIL';
test_comments="&sysmacroname: &lib..&ds..&col has &result values "
!!"not in &clib..&cds..&ccol ";
%if &test=ANYVAL %then %do;
if &result < &orig then test_result='PASS';
%end;
%else %if &test=ALLVALS %then %do;
if &result=0 then test_result='PASS';
%end;
%else %do;
test_comments="&sysmacroname: Unsatisfied test condition - &test";
%end;
run;
%local ds;
%let ds=&syslast;
proc append base=&outds data=&ds;
run;
proc sql;
drop table &ds;
%mend;

118
base/mp_assertdsobs.sas Normal file
View File

@@ -0,0 +1,118 @@
/**
@file
@brief Asserts the number of observations in a dataset
@details Useful in the context of writing sasjs tests. The results of the
test are _appended_ to the &outds. table.
Example usage:
%mp_assertdsobs(sashelp.class) %* tests if any observations are present;
<h4> SAS Macros </h4>
@li mf_nobs.sas
@li mp_abort.sas
@param [in] inds input dataset to test for presence of observations
@param [in] desc= (Testing observations) The user provided test description
@param [in] test= (HASOBS) The test to apply. Valid values are:
@li HASOBS - Test is a PASS if the input dataset has any observations
@li EMPTY - Test is a PASS if input dataset is empty
@li EQUALS [integer] - Test passes if row count matches the provided integer
@LI ATLEAST [integer] - Test passes if row count is more than or equal to
the provided integer
@LI ATMOST [integer] - Test passes if row count is less than or equal to
the provided integer
@param [out] outds= (work.test_results) The output dataset to contain the
results. If it does not exist, it will be created, with the following format:
|TEST_DESCRIPTION:$256|TEST_RESULT:$4|TEST_COMMENTS:$256|
|---|---|---|
|User Provided description|PASS|Dataset &inds has XX obs|
<h4> Related Macros </h4>
@li mp_assertcolvals.sas
@li mp_assert.sas
@li mp_assertcols.sas
@version 9.2
@author Allan Bowe
**/
%macro mp_assertdsobs(inds,
test=HASOBS,
desc=Testing observations,
outds=work.test_results
)/*/STORE SOURCE*/;
%local nobs;
%let nobs=%mf_nobs(&inds);
%let test=%upcase(&test);
%if %substr(&test.xxxxx,1,6)=EQUALS %then %do;
%let val=%scan(&test,2,%str( ));
%mp_abort(iftrue= (%DATATYP(&val)=CHAR)
,mac=&sysmacroname
,msg=%str(Invalid test - &test, expected EQUALS [integer])
)
%let test=EQUALS;
%end;
%else %if %substr(&test.xxxxxxx,1,7)=ATLEAST %then %do;
%let val=%scan(&test,2,%str( ));
%mp_abort(iftrue= (%DATATYP(&val)=CHAR)
,mac=&sysmacroname
,msg=%str(Invalid test - &test, expected ATLEAST [integer])
)
%let test=ATLEAST;
%end;
%else %if %substr(&test.xxxxxxx,1,7)=ATMOST %then %do;
%let val=%scan(&test,2,%str( ));
%mp_abort(iftrue= (%DATATYP(&val)=CHAR)
,mac=&sysmacroname
,msg=%str(Invalid test - &test, expected ATMOST [integer])
)
%let test=ATMOST;
%end;
%else %if &test ne HASOBS and &test ne EMPTY %then %do;
%mp_abort(
mac=&sysmacroname,
msg=%str(Invalid test - &test)
)
%end;
data;
length test_description $256 test_result $4 test_comments $256;
test_description=symget('desc');
test_result='FAIL';
test_comments="&sysmacroname: Dataset &inds has &nobs observations.";
test_comments=test_comments!!" Test was "!!symget('test');
%if &test=HASOBS %then %do;
if &nobs>0 then test_result='PASS';
%end;
%else %if &test=EMPTY %then %do;
if &nobs=0 then test_result='PASS';
%end;
%else %if &test=EQUALS %then %do;
if &nobs=&val then test_result='PASS';
%end;
%else %if &test=ATLEAST %then %do;
if &nobs ge &val then test_result='PASS';
%end;
%else %if &test=ATMOST %then %do;
if &nobs le &val then test_result='PASS';
%end;
%else %do;
test_comments="&sysmacroname: Unsatisfied test condition - &test";
%end;
run;
%local ds;
%let ds=&syslast;
proc append base=&outds data=&ds;
run;
proc sql;
drop table &ds;
%mend mp_assertdsobs;

View File

@@ -1,20 +1,24 @@
/**
@file
@brief Drops tables / views (if they exist) without warnings in the log
@details
@details Useful for dropping tables when you're not sure they exist, or if
you are not sure whether they are a dataset or view. Also efficient for
dropping multiple tables / views.
Example usage:
proc sql;
create table data1 as select * from sashelp.class;
create view view2 as select * from sashelp.class;
%mp_dropmembers(list=data1 view2)
%mp_dropmembers(data1 view2, libref=WORK)
<h4> SAS Macros </h4>
@li mf_isblank.sas
@param list space separated list of datasets / views
@param libref= can only drop from a single library at a time
@param list space separated list of datasets / views, WITHOUT libref
@param libref= (WORK) Note - you can only drop from a single library at a time
@version 9.2
@author Allan Bowe
@@ -35,4 +39,4 @@
delete &list;
delete &list /mtype=view;
run;
%mend;
%mend mp_dropmembers;

View File

@@ -43,7 +43,7 @@
%local i setds nvars;
%if not %sysfunc(exist(&base_ds)) %then %do;
%put WARNING: &base_ds does not exist;
%put %str(WARN)ING: &base_ds does not exist;
%return;
%end;
@@ -61,7 +61,7 @@ select count(*) into: nvars from dictionary.columns
where libname="%scan(%upcase(&base_ds),1)"
and memname="%scan(%upcase(&base_ds),2)";
%if &nvars=0 %then %do;
%put WARNING: Dataset &base_ds has no variables! It will not be converted.;
%put %str(WARN)ING: Dataset &base_ds has no variables, will not be converted.;
%return;
%end;

View File

@@ -19,7 +19,7 @@
)/*/STORE SOURCE*/;
%if not %sysfunc(exist(&ds)) %then %do;
%put WARNING: &ds does not exist;
%put %str(WARN)ING: &ds does not exist;
%return;
%end;

98
base/mp_ds2fmtds.sas Normal file
View File

@@ -0,0 +1,98 @@
/**
@file
@brief Converts every value in a dataset to it's formatted value
@details Converts every value to it's formatted value. All variables will
become character, and will be in the same order.
Usage:
%mp_ds2fmtds(sashelp.cars,work.cars)
@param [in] libds The library.dataset to be converted
@param [out] outds The dataset to create.
<h4> Related Macros <h4>
@li mp_jsonout.sas
@version 9.2
@author Allan Bowe
**/
%macro mp_ds2fmtds(libds, outds
)/*/STORE SOURCE*/;
/* validations */
%if not %sysfunc(exist(&libds)) %then %do;
%put %str(WARN)ING: &libds does not exist;
%return;
%end;
%if %index(&libds,.)=0 %then %let libds=WORK.&libds;
/* grab metadata */
proc contents noprint data=&libds
out=_data_(keep=name type length format formatl formatd varnum);
run;
proc sort;
by varnum;
run;
/* prepare formats and varnames */
data _null_;
set &syslast end=last;
name=upcase(name);
/* fix formats */
if type=2 or type=6 then do;
length fmt $49.;
if format='' then fmt=cats('$',length,'.');
else if formatl=0 then fmt=cats(format,'.');
else fmt=cats(format,formatl,'.');
newlen=max(formatl,length);
end;
else do;
if format='' then fmt='best.';
else if formatl=0 then fmt=cats(format,'.');
else if formatd=0 then fmt=cats(format,formatl,'.');
else fmt=cats(format,formatl,'.',formatd);
/* needs to be wide, for datetimes etc */
newlen=max(length,formatl,24);
end;
/* 32 char unique name */
newname='sasjs'!!substr(cats(put(md5(name),$hex32.)),1,27);
call symputx(cats('name',_n_),name,'l');
call symputx(cats('newname',_n_),newname,'l');
call symputx(cats('len',_n_),newlen,'l');
call symputx(cats('fmt',_n_),fmt,'l');
call symputx(cats('type',_n_),type,'l');
if last then call symputx('nobs',_n_,'l');
run;
/* clean up */
proc sql;
drop table &syslast;
%if &nobs=0 %then %do;
%put Dataset &libds has no columns!
data &outds;
set &libds;
run;
%return;
%end;
data &outds;
/* rename on entry */
set &libds(rename=(
%local i;
%do i=1 %to &nobs;
&&name&i=&&newname&i
%end;
));
%do i=1 %to &nobs;
length &&name&i $&&len&i;
&&name&i=left(put(&&newname&i,&&fmt&i));
drop &&newname&i;
%end;
if _error_ then call symputx('syscc',1012);
run;
%mend mp_ds2fmtds;

203
base/mp_filtercheck.sas Normal file
View File

@@ -0,0 +1,203 @@
/**
@file
@brief Checks an input filter table for validity
@details Performs checks on the input table to ensure it arrives in the
correct format. This is necessary to prevent code injection. Will update
SYSCC to 1008 if bad records are found, and call mp_abort.sas for a
graceful service exit (configurable).
Used for dynamic filtering in [Data Controller for SAS&reg;](https://datacontroller.io).
Usage:
%mp_filtercheck(work.filter,targetds=sashelp.class,outds=work.badrecords)
The input table should have the following format:
|GROUP_LOGIC:$3|SUBGROUP_LOGIC:$3|SUBGROUP_ID:8.|VARIABLE_NM:$32|OPERATOR_NM:$10|RAW_VALUE:$4000|
|---|---|---|---|---|---|
|AND|AND|1|AGE|=|12|
|AND|AND|1|SEX|<=|'M'|
|AND|OR|2|Name|NOT IN|('Jane','Alfred')|
|AND|OR|2|Weight|>=|7|
Rules applied:
@li GROUP_LOGIC - only AND/OR
@li SUBGROUP_LOGIC - only AND/OR
@li SUBGROUP_ID - only integers
@li VARIABLE_NM - must be in the target table
@li OPERATOR_NM - only =/>/</<=/>=/BETWEEN/IN/NOT IN/NE/CONTAINS
@li RAW_VALUE - no unquoted values except integers, commas and spaces.
@returns The &outds table containing any bad rows, plus a REASON_CD column.
@param [in] inds The table to be checked, with the format above
@param [in] targetds= The target dataset against which to verify VARIABLE_NM.
This must be available (ie, the library must be assigned).
@param [out] abort= (YES) If YES will call mp_abort.sas on any exceptions
@param [out] outds= The output table, which is a copy of the &inds. table
plus a REASON_CD column, containing only bad records. If bad records found,
the SYSCC value will be set to 1008 (general data problem). Downstream
processes should check this table (and return code) before continuing.
<h4> SAS Macros </h4>
@li mp_abort.sas
@li mf_getuniquefileref.sas
@li mf_getvarlist.sas
@li mf_getvartype.sas
@li mp_filtergenerate.sas
@li mp_filtervalidate.sas
<h4> Related Macros </h4>
@li mp_filtergenerate.sas
@li mp_filtervalidate.sas
@version 9.3
@author Allan Bowe
@todo Support date / hex / name literals and exponents in RAW_VALUE field
**/
%macro mp_filtercheck(inds,targetds=,outds=work.badrecords,abort=YES);
%mp_abort(iftrue= (&syscc ne 0)
,mac=&sysmacroname
,msg=%str(syscc=&syscc - on macro entry)
)
/* Validate input column */
%local vtype;
%let vtype=%mf_getvartype(&inds,RAW_VALUE);
%mp_abort(iftrue=(&abort=YES and &vtype ne C),
mac=&sysmacroname,
msg=%str(%str(ERR)OR: RAW_VALUE must be character)
)
%if &vtype ne C %then %do;
%put &sysmacroname: RAW_VALUE must be character;
%let syscc=42;
%return;
%end;
/**
* Sanitise the values based on valid value lists, then strip out
* quotes, commas, periods and spaces.
* Only numeric values should remain
*/
%local reason_cd nobs;
%let nobs=0;
data &outds;
/*length GROUP_LOGIC SUBGROUP_LOGIC $3 SUBGROUP_ID 8 VARIABLE_NM $32
OPERATOR_NM $10 RAW_VALUE $4000;*/
set &inds;
length reason_cd $4032;
/* closed list checks */
if GROUP_LOGIC not in ('AND','OR') then do;
REASON_CD='GROUP_LOGIC should be AND/OR, not:'!!cats(GROUP_LOGIC);
putlog REASON_CD= GROUP_LOGIC=;
call symputx('reason_cd',reason_cd,'l');
call symputx('nobs',_n_,'l');
output;
end;
if SUBGROUP_LOGIC not in ('AND','OR') then do;
REASON_CD='SUBGROUP_LOGIC should be AND/OR, not:'!!cats(SUBGROUP_LOGIC);
putlog REASON_CD= SUBGROUP_LOGIC=;
call symputx('reason_cd',reason_cd,'l');
call symputx('nobs',_n_,'l');
output;
end;
if mod(SUBGROUP_ID,1) ne 0 then do;
REASON_CD='SUBGROUP_ID should be integer, not '!!left(subgroup_id);
putlog REASON_CD= SUBGROUP_ID=;
call symputx('reason_cd',reason_cd,'l');
call symputx('nobs',_n_,'l');
output;
end;
if upcase(VARIABLE_NM) not in
(%upcase(%mf_getvarlist(&targetds,dlm=%str(,),quote=SINGLE)))
then do;
REASON_CD="Variable "!!cats(variable_nm)!!" not in &targetds";
putlog REASON_CD= VARIABLE_NM=;
call symputx('reason_cd',reason_cd,'l');
call symputx('nobs',_n_,'l');
output;
end;
if OPERATOR_NM not in
('=','>','<','<=','>=','BETWEEN','IN','NOT IN','NE','CONTAINS')
then do;
REASON_CD='Invalid OPERATOR_NM: '!!left(OPERATOR_NM);
putlog REASON_CD= OPERATOR_NM=;
call symputx('reason_cd',reason_cd,'l');
call symputx('nobs',_n_,'l');
output;
end;
/* special logic */
if OPERATOR_NM='BETWEEN' then raw_value1=tranwrd(raw_value,' AND ','');
else if OPERATOR_NM in ('IN','NOT IN') then do;
if substr(raw_value,1,1) ne '('
or substr(cats(reverse(raw_value)),1,1) ne ')'
then do;
REASON_CD='Missing start/end bracket in RAW_VALUE';
putlog REASON_CD= OPERATOR_NM= raw_value= raw_value1= ;
call symputx('reason_cd',reason_cd,'l');
call symputx('nobs',_n_,'l');
output;
end;
else raw_value1=substr(raw_value,2,max(length(raw_value)-2,0));
end;
else raw_value1=raw_value;
/* remove nested literals eg '' */
raw_value1=tranwrd(raw_value1,"''",'');
/* now match string literals (always single quotes) */
raw_value2=raw_value1;
regex = prxparse("s/(\').*?(\')//");
call prxchange(regex,-1,raw_value2);
/* remove commas and periods*/
raw_value3=compress(raw_value2,',.');
/* output records that contain values other than digits and spaces */
if notdigit(compress(raw_value3,' '))>0 then do;
putlog raw_value3= $hex32.;
REASON_CD=cats('Invalid RAW_VALUE:',raw_value);
putlog REASON_CD= raw_value= raw_value1= raw_value2= raw_value3=;
call symputx('reason_cd',reason_cd,'l');
call symputx('nobs',_n_,'l');
output;
end;
run;
data _null_;
set &outds end=last;
putlog (_all_)(=);
run;
%mp_abort(iftrue=(&abort=YES and &nobs>0),
mac=&sysmacroname,
msg=%str(Data issue: %superq(reason_cd))
)
%if &nobs>0 %then %do;
%let syscc=1008;
%return;
%end;
/**
* syntax checking passed but it does not mean the filter is valid
* for that we can run a proc sql validate query
*/
%local fref1;
%let fref1=%mf_getuniquefileref();
%mp_filtergenerate(&inds,outref=&fref1)
/* this macro will also set syscc to 1008 if any issues found */
%mp_filtervalidate(&fref1,&targetds,outds=&outds,abort=&abort)
%mend mp_filtercheck;

102
base/mp_filtergenerate.sas Normal file
View File

@@ -0,0 +1,102 @@
/**
@file
@brief Generates a filter clause from an input table, to a fileref
@details Uses the input table to generate an output filter clause.
This feature is used to create dynamic dropdowns in [Data Controller for SAS&reg](
https://datacontroller.io). The input table should be in the format below:
|GROUP_LOGIC:$3|SUBGROUP_LOGIC:$3|SUBGROUP_ID:8.|VARIABLE_NM:$32|OPERATOR_NM:$10|RAW_VALUE:$4000|
|---|---|---|---|---|---|
|AND|AND|1|AGE|=|12|
|AND|AND|1|SEX|<=|'M'|
|AND|OR|2|Name|NOT IN|('Jane','Alfred')|
|AND|OR|2|Weight|>=|7|
Note - if the above table is received from an external client, the values
should first be validated using the mp_filtercheck.sas macro to avoid risk
of SQL injection.
To generate the filter, run the following code:
data work.filtertable;
infile datalines4 dsd;
input GROUP_LOGIC:$3. SUBGROUP_LOGIC:$3. SUBGROUP_ID:8. VARIABLE_NM:$32.
OPERATOR_NM:$10. RAW_VALUE:$4000.;
datalines4;
AND,AND,1,AGE,=,12
AND,AND,1,SEX,<=,"'M'"
AND,OR,2,Name,NOT IN,"('Jane','Alfred')"
AND,OR,2,Weight,>=,7
;;;;
run;
%mp_filtergenerate(work.filtertable,outref=myfilter)
data _null_;
infile myfilter;
input;
put _infile_;
run;
Will write the following query to the log:
> (
> AGE = 12
> AND
> SEX <= 'M'
> ) AND (
> Name NOT IN ('Jane','Alfred')
> OR
> Weight >= 7
> )
@param [in] inds The input table with query values
@param [out] outref= The output fileref to contain the filter clause. Will
be created (or replaced).
<h4> Related Macros </h4>
@li mp_filtercheck.sas
@li mp_filtervalidate.sas
<h4> SAS Macros </h4>
@li mp_abort.sas
@li mf_nobs.sas
@version 9.3
@author Allan Bowe
**/
%macro mp_filtergenerate(inds,outref=filter);
%mp_abort(iftrue= (&syscc ne 0)
,mac=&sysmacroname
,msg=%str(syscc=&syscc - on macro entry)
)
filename &outref temp;
%if %mf_nobs(&inds)=0 %then %do;
/* ensure we have a default filter */
data _null_;
file &outref;
put '1=1';
run;
%end;
%else %do;
data _null_;
file &outref lrecl=32800;
set &inds end=last;
by SUBGROUP_ID;
if _n_=1 then put '((';
else if first.SUBGROUP_ID then put +1 GROUP_LOGIC '(';
else put +2 SUBGROUP_LOGIC;
put +4 VARIABLE_NM OPERATOR_NM RAW_VALUE;
if last.SUBGROUP_ID then put ')'@;
if last then put ')';
run;
%end;
%mend;

105
base/mp_filtervalidate.sas Normal file
View File

@@ -0,0 +1,105 @@
/**
@file
@brief Checks a generated filter query for validity
@details Runs a generated filter in proc sql with the validate option.
Used in mp_filtercheck.sas in an fcmp container.
Built to support dynamic filtering in
[Data Controller for SAS&reg;](https://datacontroller.io).
Usage:
data work.filtertable;
infile datalines4 dsd;
input GROUP_LOGIC:$3. SUBGROUP_LOGIC:$3. SUBGROUP_ID:8. VARIABLE_NM:$32.
OPERATOR_NM:$10. RAW_VALUE:$4000.;
datalines4;
AND,AND,1,AGE,=,12
AND,AND,1,SEX,<=,"'M'"
AND,OR,2,Name,NOT IN,"('Jane','Alfred')"
AND,OR,2,Weight,>=,7
;;;;
run;
%mp_filtergenerate(work.filtertable,outref=myfilter)
%mp_filtervalidate(myfilter,sashelp.class)
@returns The SYSCC value will be 1008 if there are validation issues.
@param [in] inref The input fileref to validate (generated by
mp_filtergenerate.sas)
@param [in] targetds The target dataset against which to verify the query
@param [out] abort= (YES) If YES will call mp_abort.sas on any exceptions
@param [out] outds= (work.mp_filtervalidate) Output dataset containing the
error / warning message, if one exists. If this table contains any rows,
there are problems!
<h4> SAS Macros </h4>
@li mf_getuniquefileref.sas
@li mf_nobs.sas
@li mp_abort.sas
<h4> Related Macros </h4>
@li mp_filtercheck.sas
@li mp_filtergenerate.sas
@version 9.3
@author Allan Bowe
**/
%macro mp_filtervalidate(inref,targetds,abort=YES,outds=work.mp_filtervalidate);
%mp_abort(iftrue= (&syscc ne 0 or &syserr ne 0)
,mac=&sysmacroname
,msg=%str(syscc=&syscc / syserr=&syserr - on macro entry)
)
%local fref1;
%let fref1=%mf_getuniquefileref();
data _null_;
file &fref1;
infile &inref end=eof;
if _n_=1 then do;
put "proc sql;";
put "validate select * from &targetds";
put "where " ;
end;
input;
put _infile_;
putlog _infile_;
if eof then put ";quit;";
run;
%inc &fref1;
data &outds;
if &sqlrc or &syscc or &syserr then do;
REASON_CD=coalescec(symget('SYSERRORTEXT'),symget('SYSWARNINGTEXT'));
output;
end;
else stop;
run;
filename &fref1 clear;
%if %mf_nobs(&outds)>0 %then %do;
%if &abort=YES %then %do;
data _null_;
set &outds;
call symputx('REASON_CD',reason_cd,'l');
stop;
run;
%mp_abort(
mac=&sysmacroname,
msg=%str(Filter validation issues. ERR=%superq(SYSERRORTEXT)
, WARN=%superq(SYSWARNINGTEXT) )
)
%end;
%let syscc=1008;
%end;
%mend;

75
base/mp_hashdataset.sas Normal file
View File

@@ -0,0 +1,75 @@
/**
@file
@brief Returns a unique hash for a dataset
@details Ignores metadata attributes, used only to hash values. Compared
datasets must be in the same order.
%mp_hashdataset(sashelp.class,outds=myhash)
data _null_;
set work.myhash;
put hashkey=;
run;
![sas md5 hash dataset log results](https://i.imgur.com/MqF98vk.png)
<h4> SAS Macros </h4>
@li mf_getattrn.sas
@li mf_getuniquename.sas
@li mf_getvarlist.sas
@li mf_getvartype.sas
@param [in] libds dataset to hash
@param [out] outds= (work.mf_hashdataset) The output dataset to create. This
will contain one column (hashkey) with one observation (a hex32.
representation of the input hash)
|hashkey:$32.|
|---|
|28ABC74ABFC45F50794237BA5566E6CA|
@version 9.2
@author Allan Bowe
**/
%macro mp_hashdataset(
libds,
outds=
)/*/STORE SOURCE*/;
%if %mf_getattrn(&libds,NLOBS)=0 %then %do;
%put %str(WARN)ING: Dataset &libds is empty;, or is not a dataset;
%end;
%else %if %mf_getattrn(&libds,NLOBS)<0 %then %do;
%put %str(ERR)OR: Dataset &libds is not a dataset;
%end;
%else %do;
%local keyvar /* roll up the md5 */
prevkeyvar /* retain prev record md5 */
lastvar /* last var in input ds */
varlist var i;
/* avoid naming conflict for hash key vars */
%let keyvar=%mf_getuniquename();
%let prevkeyvar=%mf_getuniquename();
%let lastvar=%mf_getuniquename();
%let varlist=%mf_getvarlist(&libds);
data &outds(rename=(&keyvar=hashkey) keep=&keyvar);
length &prevkeyvar &keyvar $32;
retain &prevkeyvar;
set &libds end=&lastvar;
/* hash should include previous row */
&keyvar=put(md5(&prevkeyvar
/* loop every column, hashing every individual value */
%do i=1 %to %sysfunc(countw(&varlist));
%let var=%scan(&varlist,&i,%str( ));
%if %mf_getvartype(&libds,&var)=C %then %do;
!!put(md5(trim(&var)),$hex32.)
%end;
%else %do;
!!put(md5(trim(put(&var*1,binary64.))),$hex32.)
%end;
%end;
),$hex32.);
&prevkeyvar=&keyvar;
if &lastvar then output;
run;
%end;
%mend;

View File

@@ -48,6 +48,8 @@
@param dbg= DEPRECATED - was used to conditionally add PRETTY to
proc json but this can cause line truncation in large files.
<h4> Related Macros <h4>
@li mp_ds2fmtds.sas
@version 9.2
@author Allan Bowe
@@ -55,10 +57,11 @@
**/
%macro mp_jsonout(action,ds,jref=_webout,dslabel=,fmt=Y,engine=PROCJSON,dbg=0
%macro mp_jsonout(action,ds,jref=_webout,dslabel=,fmt=Y,engine=DATASTEP,dbg=0
)/*/STORE SOURCE*/;
%put output location=&jref;
%if &action=OPEN %then %do;
options nobomfile;
data _null_;file &jref encoding='utf-8';
put '{"START_DTTM" : "' "%sysfunc(datetime(),datetime20.3)" '"';
run;
@@ -86,6 +89,64 @@
%put &sysmacroname: &ds NOT FOUND!!!;
%return;
%end;
%if &fmt=Y %then %do;
%put converting every variable to a formatted variable;
/* see mp_ds2fmtds.sas for source */
proc contents noprint data=&ds
out=_data_(keep=name type length format formatl formatd varnum);
run;
proc sort;
by varnum;
run;
%local fmtds;
%let fmtds=%scan(&syslast,2,.);
/* prepare formats and varnames */
data _null_;
set &fmtds end=last;
name=upcase(name);
/* fix formats */
if type=2 or type=6 then do;
length fmt $49.;
if format='' then fmt=cats('$',length,'.');
else if formatl=0 then fmt=cats(format,'.');
else fmt=cats(format,formatl,'.');
newlen=max(formatl,length);
end;
else do;
if format='' then fmt='best.';
else if formatl=0 then fmt=cats(format,'.');
else if formatd=0 then fmt=cats(format,formatl,'.');
else fmt=cats(format,formatl,'.',formatd);
/* needs to be wide, for datetimes etc */
newlen=max(length,formatl,24);
end;
/* 32 char unique name */
newname='sasjs'!!substr(cats(put(md5(name),$hex32.)),1,27);
call symputx(cats('name',_n_),name,'l');
call symputx(cats('newname',_n_),newname,'l');
call symputx(cats('len',_n_),newlen,'l');
call symputx(cats('fmt',_n_),fmt,'l');
call symputx(cats('type',_n_),type,'l');
if last then call symputx('nobs',_n_,'l');
run;
data &fmtds;
/* rename on entry */
set &ds(rename=(
%local i;
%do i=1 %to &nobs;
&&name&i=&&newname&i
%end;
));
%do i=1 %to &nobs;
length &&name&i $&&len&i;
&&name&i=left(put(&&newname&i,&&fmt&i));
drop &&newname&i;
%end;
if _error_ then call symputx('syscc',1012);
run;
%let ds=&fmtds;
%end; /* &fmt=Y */
data _null_;file &jref mod ;
put "["; call symputx('cols',0,'l');
proc sort
@@ -169,4 +230,4 @@
put "}";
run;
%end;
%mend;
%mend mp_jsonout;

View File

@@ -9,7 +9,15 @@
%mp_searchcols(libs=sashelp work, cols=name sex age)
@param libs=
@param libs=(SASHELP) Space separated list of libraries to search for columns
@param cols= Space separated list of column names to search for (not case
sensitive)
@param outds=(mp_searchcols) the table to create with the results. Will have
one line per table match.
@param match=(ANY) The match type. Valid values:
@li ANY - The table contains at least one of the columns
@li WILD - The table contains a column with a name that partially matches
@version 9.2
@author Allan Bowe
**/
@@ -17,6 +25,7 @@
%macro mp_searchcols(libs=sashelp
,cols=
,outds=mp_searchcols
,match=ANY
)/*/STORE SOURCE*/;
%put &sysmacroname process began at %sysfunc(datetime(),datetime19.);
@@ -38,8 +47,10 @@ create table _data_ as
%end;
order by 1,2,3;
%local tempds;
%let tempds=&syslast;
data &outds;
set &syslast;
set &tempds;
length cols matchcols $32767;
cols=upcase(symget('cols'));
colcount=countw(cols);
@@ -53,10 +64,29 @@ data &outds;
retain matchcols;
matchcols='';
end;
%if &match=ANY %then %do;
if findw(cols,name,,'spit') then do;
sumcols+1;
matchcols=cats(matchcols)!!' '!!cats(name);
end;
%end;
%else %if &match=WILD %then %do;
if _n_=1 then do;
retain wcount;
wcount=countw(cols);
drop wcount;
end;
do i=1 to wcount;
length curword $32;
curword=scan(cols,i,' ');
drop curword;
if index(name,cats(curword)) then do;
sumcols+1;
matchcols=cats(matchcols)!!' '!!cats(curword);
end;
end;
%end;
if last.memname then do;
if sumcols>0 then output;
if sumcols=colcount then putlog "Full Match: " libname memname;
@@ -66,6 +96,8 @@ run;
proc sort; by descending sumcols memname libname; run;
proc sql;
drop table &tempds;
%put &sysmacroname process finished at %sysfunc(datetime(),datetime19.);
%mend;
%mend mp_searchcols;

View File

@@ -36,8 +36,20 @@
%let contentype=%upcase(&contenttype);
%local platform; %let platform=%mf_getplatform();
/**
* check engine type to avoid the below err message:
* > Function is only valid for filerefs using the CACHE access method.
*/
%local streamweb;
%let streamweb=0;
data _null_;
set sashelp.vextfl(where=(upcase(fileref)="_WEBOUT"));
if xengine='STREAM' then call symputx('streamweb',1,'l');
run;
%if &contentype=ZIP %then %do;
%if &platform=SASMETA %then %do;
%if &platform=SASMETA and &streamweb=1 %then %do;
data _null_;
rc=stpsrv_header('Content-type','application/zip');
rc=stpsrv_header('Content-disposition',"attachment; filename=&outname");
@@ -51,7 +63,7 @@
%end;
%else %if &contentype=EXCEL %then %do;
/* suitable for XLS format */
%if &platform=SASMETA %then %do;
%if &platform=SASMETA and &streamweb=1 %then %do;
data _null_;
rc=stpsrv_header('Content-type','application/vnd.ms-excel');
rc=stpsrv_header('Content-disposition',"attachment; filename=&outname");
@@ -64,7 +76,7 @@
%end;
%end;
%else %if &contentype=XLSX %then %do;
%if &platform=SASMETA %then %do;
%if &platform=SASMETA and &streamweb=1 %then %do;
data _null_;
rc=stpsrv_header('Content-type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
@@ -79,7 +91,7 @@
%end;
%end;
%else %if &contentype=TEXT %then %do;
%if &platform=SASMETA %then %do;
%if &platform=SASMETA and &streamweb=1 %then %do;
data _null_;
rc=stpsrv_header('Content-type','application/text');
rc=stpsrv_header('Content-disposition',"attachment; filename=&outname");
@@ -92,7 +104,7 @@
%end;
%end;
%else %if &contentype=CSV %then %do;
%if &platform=SASMETA %then %do;
%if &platform=SASMETA and &streamweb=1 %then %do;
data _null_;
rc=stpsrv_header('Content-type','application/csv');
rc=stpsrv_header('Content-disposition',"attachment; filename=&outname");
@@ -122,4 +134,4 @@
%mp_binarycopy(inloc="&inloc",outref=_webout)
%end;
%mend;
%mend;

259
base/mp_testservice.sas Normal file
View File

@@ -0,0 +1,259 @@
/**
@file mp_testservice.sas
@brief Will execute a test against a SASjs web service on SAS 9 or Viya
@details Prepares the input files and retrieves the resulting datasets from
the response JSON.
%mp_testjob(
duration=60*5
)
Note - the _webout fileref should NOT be assigned prior to running this macro.
@param [in] program The _PROGRAM endpoint to test
@param [in] inputfiles=(0) A list of space seperated fileref:filename pairs as
follows:
inputfiles=inref:filename inref2:filename2
@param [in] inputparams=(0) A dataset containing name/value pairs in the
following format:
|name:$32|value:$1000|
|---|---|
|stpmacname|some value|
|mustbevalidname|can be anything, oops, %abort!!|
@param [in] debug= (log) Provide the _debug value
@param [in] viyaresult=(WEBOUT_JSON) The Viya result type to return. For
more info, see mv_getjobresult.sas
@param [out] outlib= (0) Output libref to contain the final tables. Set to
0 if the service output is not in JSON format.
@param [out] outref= (0) Output fileref to create, to contain the full _webout
response.
<h4> SAS Macros </h4>
@li mf_getplatform.sas
@li mf_getuniquefileref.sas
@li mf_getuniquename.sas
@li mp_abort.sas
@li mp_binarycopy.sas
@li mv_getjobresult.sas
@li mv_jobflow.sas
@version 9.4
@author Allan Bowe
**/
%macro mp_testservice(program,
inputfiles=0,
inputparams=0,
debug=log,
outlib=0,
outref=0,
viyaresult=WEBOUT_JSON
)/*/STORE SOURCE*/;
%local mdebug;
%if &debug ne 0 %then %do;
%let mdebug=1;
%put &sysmacroname entry vars:;
%put _local_;
%end;
%else %let mdebug=0;
/* sanitise inputparams */
%local pcnt;
%let pcnt=0;
%if &inputparams ne 0 %then %do;
data _null_;
set &inputparams;
if not nvalid(name,'v7') then putlog (_all_)(=);
else if name in (
'program','inputfiles','inputparams','debug','outlib','outref'
) then putlog (_all_)(=);
else do;
x+1;
call symputx(name,quote(cats(value)),'l');
call symputx('pval'!!left(x),name,'l');
call symputx('pcnt',x,'l');
end;
run;
%mp_abort(iftrue= (%mf_nobs(&inputparams) ne &pcnt)
,mac=&sysmacroname
,msg=%str(Invalid values in &inputparams)
)
%end;
%local fref1 webref;
%let fref1=%mf_getuniquefileref();
%let webref=%mf_getuniquefileref();
%local platform;
%let platform=%mf_getplatform();
%if &platform=SASMETA %then %do;
/* parse the input files */
%local webcount i var;
%if %quote(&inputfiles) ne 0 %then %do;
%let webcount=%sysfunc(countw(&inputfiles));
%put &=webcount;
%do i=1 %to &webcount;
%let var=%scan(&inputfiles,&i,%str( ));
%local webfref&i webname&i;
%let webref&i=%scan(&var,1,%str(:));
%let webname&i=%scan(&var,2,%str(:));
%put webref&i=&&webref&i;
%put webname&i=&&webname&i;
%end;
%end;
%else %let webcount=0;
proc stp program="&program";
inputparam _program="&program"
%do i=1 %to &webcount;
%if &webcount=1 %then %do;
_webin_fileref="&&webref&i"
_webin_name="&&webname&i"
%end;
%else %do;
_webin_fileref&i="&&webref&i"
_webin_name&i="&&webname&i"
%end;
%end;
_webin_file_count="&webcount"
_debug="&debug"
%do i=1 %to &pcnt;
/* resolve name only, proc stp fetches value */
&&pval&i=&&&&&&pval&i
%end;
;
%do i=1 %to &webcount;
inputfile &&webref&i;
%end;
outputfile _webout=&webref;
run;
data _null_;
infile &webref;
file &fref1;
input;
length line $10000;
if index(_infile_,'>>weboutBEGIN<<') then do;
line=tranwrd(_infile_,'>>weboutBEGIN<<','');
put line;
end;
else if index(_infile_,'>>weboutEND<<') then do;
line=tranwrd(_infile_,'>>weboutEND<<','');
put line;
stop;
end;
else put _infile_;
run;
data _null_;
infile &fref1;
input;
put _infile_;
run;
%if &outlib ne 0 %then %do;
libname &outlib json (&fref1);
%end;
%if &outref ne 0 %then %do;
filename &outref temp;
%mp_binarycopy(inref=&webref,outref=&outref)
%end;
%end;
%else %if &platform=SASVIYA %then %do;
/* prepare inputparams */
%local ds1;
%let ds1=%mf_getuniquename();
%if "&inputparams" ne "0" %then %do;
proc transpose data=&inputparams out=&ds1;
id name;
var value;
run;
%end;
%else %do;
data &ds1;run;
%end;
/* parse the input files - convert to sasjs params */
%local webcount i var sasjs_tables;
%if %quote(&inputfiles) ne 0 %then %do;
%let webcount=%sysfunc(countw(&inputfiles));
%put &=webcount;
%do i=1 %to &webcount;
%let var=%scan(&inputfiles,&i,%str( ));
%local webfref&i webname&i sasjs&i.data;
%let webref&i=%scan(&var,1,%str(:));
%let webname&i=%scan(&var,2,%str(:));
%put webref&i=&&webref&i;
%put webname&i=&&webname&i;
%let sasjs_tables=&sasjs_tables &&webname&i;
data _null_;
infile &&webref&i lrecl=32767;
input;
if _n_=1 then call symputx("sasjs&i.data",_infile_);
else call symputx(
"sasjs&i.data",cats(symget("sasjs&i.data"),'0D0A'x,_infile_)
);
putlog "&sysmacroname infile: " _infile_;
run;
data &ds1;
set &ds1;
length sasjs&i.data $32767 sasjs_tables $1000;
sasjs&i.data=symget("sasjs&i.data");
sasjs_tables=symget("sasjs_tables");
run;
%end;
%end;
%else %let webcount=0;
data &ds1;
retain _program "&program";
set &ds1;
putlog "&sysmacroname inputparams:";
putlog (_all_)(=);
run;
%mv_jobflow(inds=&ds1
,maxconcurrency=1
,outds=work.results
,outref=&fref1
,mdebug=&mdebug
)
/* show the log */
data _null_;
infile &fref1;
input;
putlog _infile_;
run;
/* get the uri to fetch results */
data _null_;
set work.results;
call symputx('uri',uri);
putlog "&sysmacroname: fetching results for " uri;
run;
/* fetch results from webout.json */
%mv_getjobresult(uri=&uri,
result=&viyaresult,
outref=&outref,
outlib=&outlib,
mdebug=&mdebug
)
%end;
%else %do;
%put %str(ERR)OR: Unrecognised platform: &platform;
%end;
%if &mdebug=0 %then %do;
filename &webref clear;
%end;
%else %do;
%put &sysmacroname exit vars:;
%put _local_;
%end;
%mend mp_testservice;

66
base/mp_validatecol.sas Normal file
View File

@@ -0,0 +1,66 @@
/**
@file
@brief Used to validate variables in a dataset
@details Useful when sanitising inputs, to ensure that they arrive with a
certain pattern.
Usage:
data test;
infile datalines4 dsd;
input;
libds=_infile_;
%mp_validatecol(libds,LIBDS,is_libds)
datalines4;
some.libname
!lib.blah
%abort
definite.ok
not.ok!
nineletrs._
;;;;
run;
@param [in] incol The column to be validated
@param [in] rule The rule to apply. Current rules:
@li ISNUM - checks if the variable is numeric
@li LIBDS - matches LIBREF.DATASET format
@param [out] outcol The variable to create, with the results of the match
<h4> SAS Macros </h4>
@li mf_getuniquename.sas
@version 9.3
**/
%macro mp_validatecol(incol,rule,outcol);
/* tempcol is given a unique name with every invocation */
%local tempcol;
%let tempcol=%mf_getuniquename();
%if &rule=ISNUM %then %do;
/*
credit SØREN LASSEN
https://sasmacro.blogspot.com/2009/06/welcome-isnum-macro.html
*/
&tempcol=input(&incol,?? best32.);
if missing(&tempcol) then &outcol=0;
else &outcol=1;
drop &tempcol;
%end;
%else %if &rule=LIBDS %then %do;
/* match libref.dataset */
if _n_=1 then do;
retain &tempcol;
&tempcol=prxparse('/^[_a-z]\w{0,7}\.[_a-z]\w{0,31}$/i');
if missing(&tempcol) then do;
putlog "%str(ERR)OR: Invalid expression for LIBDS";
stop;
end;
drop &tempcol;
end;
if prxmatch(&tempcol, trim(&incol)) then &outcol=1;
else &outcol=0;
%end;
%mend mp_validatecol;

View File

@@ -2,11 +2,11 @@
@file
@brief Creates a zip file
@details For DIRECTORY usage, will ignore subfolders. For DATASET usage,
provide a column that contains the full file path to each file to be zipped.
provide a column that contains the full file path to each file to be zipped.
%mp_zip(in=myzips,type=directory,outname=myDir)
%mp_zip(in=/my/file/path.txt,type=FILE,outname=myFile)
%mp_zip(in=SOMEDS,incol=FPATH,type=DATASET,outname=myFile)
%mp_zip(in=myzips,type=directory,outname=myDir)
%mp_zip(in=/my/file/path.txt,type=FILE,outname=myFile)
%mp_zip(in=SOMEDS,incol=FPATH,type=DATASET,outname=myFile)
If you are sending zipped output to the _webout destination as part of an STP
be sure that _debug is not set (else the SPWA will send non zipped content

View File

@@ -1,3 +1,5 @@
-- NOTE - THE COPYRIGHT BELOW IS IN RELATION TO THE JSON.LUA FILE ONLY
-- THIS FILE STARTS ON THE NEXT LINE AND WILL FINISH WITH "JSON.LUA ENDS HERE"
--
-- json.lua
--
@@ -369,3 +371,5 @@ function json.decode(str)
end
return json
-- JSON.LUA ENDS HERE

View File

@@ -12,6 +12,8 @@
%macro ml_json();
data _null_;
file "%sysfunc(pathname(work))/ml_json.lua";
put '-- NOTE - THE COPYRIGHT BELOW IS IN RELATION TO THE JSON.LUA FILE ONLY ';
put '-- THIS FILE STARTS ON THE NEXT LINE AND WILL FINISH WITH "JSON.LUA ENDS HERE" ';
put '-- ';
put '-- json.lua ';
put '-- ';
@@ -383,6 +385,8 @@ data _null_;
put 'end ';
put ' ';
put 'return json ';
put ' ';
put '-- JSON.LUA ENDS HERE ';
run;
%inc "%sysfunc(pathname(work))/ml_json.lua";

View File

@@ -70,7 +70,7 @@ run;
%end;
%if &syscc ge 4 %then %do;
%put WARNING: SYSCC=&syscc, exiting &sysmacroname;
%put %str(WARN)ING: SYSCC=&syscc, exiting &sysmacroname;
%return;
%end;

View File

@@ -332,7 +332,7 @@ run;
run;
%if %length(&open_passthrough)>0 %then %do;
%put WARNING: Passthrough option for postgres not yet supported;
%put %str(WARN)ING: Passthrough option for postgres not yet supported;
%return;
%end;
%else %do;
@@ -455,8 +455,8 @@ run;
%return;
%end;
%else %do;
%put WARNING: Engine &engine is currently unsupported;
%put WARNING- Please contact your support team.;
%put %str(WARN)ING: Engine &engine is currently unsupported;
%put %str(WARN)ING- Please contact your support team.;
%return;
%end;

View File

@@ -88,7 +88,7 @@ data _null_;
putlog (_all_)(=);
run;
%if &checktype = SASLibrary %then %do;
%put WARNING: Library (&liburi) already exists with libname (&libname) ;
%put %str(WARN)ING: Library (&liburi) already exists with libname (&libname);
%return;
%end;
@@ -103,7 +103,7 @@ data _null_;
putlog (_all_)(=);
run;
%if &checktype = SASLibrary %then %do;
%put WARNING: Library (&liburi) already exists with libref (&libref) ;
%put %str(WARN)ING: Library (&liburi) already exists with libref (&libref) ;
%return;
%end;
@@ -123,7 +123,7 @@ data _null_;
call symputx('treeuri',uri,'l');
run;
%if &foldertype ne Tree %then %do;
%put WARNING: Tree &tree does not exist!;
%put %str(WARN)ING: Tree &tree does not exist!;
%return;
%end;
@@ -231,7 +231,7 @@ filename &frefout temp;
* check SAS version
*/
%if %sysevalf(&sysver lt 9.3) %then %do;
%put WARNING: Version 9.3 or later required;
%put %str(WARN)ING: Version 9.3 or later required;
%return;
%end;

View File

@@ -118,7 +118,7 @@ data _null_;
call symputx('treeuri',uri,'l');
run;
%if &foldertype ne Tree %then %do;
%put WARNING: Tree &tree does not exist!;
%put %str(WARN)ING: Tree &tree does not exist!;
%return;
%end;
@@ -133,7 +133,7 @@ data _null_;
call symputx('stpuri',uri,'l');
run;
%if &cmtype = ClassifierMap %then %do;
%put WARNING: Stored Process &stpname already exists in &tree!;
%put %str(WARN)ING: Stored Process &stpname already exists in &tree!;
%return;
%end;
@@ -141,14 +141,14 @@ run;
* Check that the physical file exists
*/
%if %sysfunc(fileexist(&directory/&filename)) ne 1 %then %do;
%put WARNING: FILE *&directory/&filename* NOT FOUND!;
%put %str(WARN)ING: FILE *&directory/&filename* NOT FOUND!;
%return;
%end;
%if &stptype=1 %then %do;
/* type 1 STP - where code is stored on filesystem */
%if %sysevalf(&sysver lt 9.2) %then %do;
%put WARNING: Version 9.2 or later required;
%put %str(WARN)ING: Version 9.2 or later required;
%return;
%end;
@@ -162,7 +162,7 @@ run;
%if &checkdirtype ne Directory %then %do;
%mm_getdirectories(path=&directory,outds=&outds ,mDebug=&mDebug)
%if %mf_nobs(&outds)=0 or %sysfunc(exist(&outds))=0 %then %do;
%put WARNING: The directory object does not exist for &directory;
%put %str(WARN)ING: The directory object does not exist for &directory;
%return;
%end;
%end;
@@ -180,12 +180,12 @@ run;
length id $20 type $256;
__rc=metadata_resolve("&treeuri",type,id);
if type ne 'Tree' then do;
putlog "WARNING: Invalid tree URI: &treeuri";
putlog "%str(WARN)ING: Invalid tree URI: &treeuri";
stopme=1;
end;
__rc=metadata_resolve(directoryuri,type,id);
if type ne 'Directory' then do;
putlog 'WARNING: Invalid directory URI: ' directoryuri;
putlog "%str(WARN)ING: Invalid directory URI: " directoryuri;
stopme=1;
end;
@@ -194,7 +194,7 @@ run;
if type ne 'LogicalServer' then do;
__rc=metadata_getnobj("omsobj:LogicalServer?@Name='&server'",1,serveruri);
if serveruri='' then do;
putlog "WARNING: Invalid server: &server";
putlog "%str(WARN)ING: Invalid server: &server";
stopme=1;
end;
end;
@@ -217,7 +217,7 @@ run;
rc6 = METADATA_SETATTR(prompturi, 'GroupInfo',groupinfo);
if sum(of rc1-rc6) ne 0 then do;
putlog 'WARNING: Issue creating prompt.';
putlog "%str(WARN)ING: Issue creating prompt.";
if prompturi ne . then do;
putlog ' Removing orphan: ' prompturi;
rc = METADATA_DELOBJ(prompturi);
@@ -232,7 +232,7 @@ run;
rc9=METADATA_SETATTR(fileuri, 'IsARelativeName','1');
rc10=METADATA_SETASSN(fileuri, 'Directories','MODIFY',directoryuri);
if sum(of rc7-rc10) ne 0 then do;
putlog 'WARNING: Issue creating file.';
putlog "%str(WARN)ING: Issue creating file.";
if fileuri ne . then do;
putlog ' Removing orphans:' prompturi fileuri;
rc = METADATA_DELOBJ(prompturi);
@@ -251,7 +251,7 @@ run;
!!"<OutputParameters/></StoredProcess>";
rc14= METADATA_SETATTR(texturi, 'StoredText',storedtext);
if sum(of rc11-rc14) ne 0 then do;
putlog 'WARNING: Issue creating TextStore.';
putlog "%str(WARN)ING: Issue creating TextStore.";
if texturi ne . then do;
putlog ' Removing orphans: ' prompturi fileuri texturi;
rc = METADATA_DELOBJ(prompturi);
@@ -299,7 +299,7 @@ run;
%else %if &stptype=2 %then %do;
/* type 2 stp - code is stored in metadata */
%if %sysevalf(&sysver lt 9.3) %then %do;
%put WARNING: SAS version 9.3 or later required to create type2 STPs;
%put %str(WARN)ING: SAS version 9.3 or later required to create type2 STPs;
%return;
%end;
/* check we have the correct ServerContext */
@@ -311,7 +311,7 @@ run;
call symputx('serveruri',serveruri);
run;
%if &serveruri=NOTFOUND %then %do;
%put WARNING: ServerContext *&server* not found!;
%put %str(WARN)ING: ServerContext *&server* not found!;
%return;
%end;
@@ -376,15 +376,13 @@ run;
*/
%mm_updatestpsourcecode(stp=&tree/&stpname
,stpcode="&directory/&filename"
,frefin=&frefin.
,frefout=&frefout.
,mdebug=&mdebug
,minify=&minify)
%end;
%else %do;
%put WARNING: STPTYPE=*&stptype* not recognised!;
%put %str(WARN)ING: STPTYPE=*&stptype* not recognised!;
%end;
%mend;

View File

@@ -86,10 +86,11 @@ data _null_;
put "/* Created on %sysfunc(datetime(),datetime19.) by %mf_getuser() */";
/* WEBOUT BEGIN */
put ' ';
put '%macro mp_jsonout(action,ds,jref=_webout,dslabel=,fmt=Y,engine=PROCJSON,dbg=0 ';
put '%macro mp_jsonout(action,ds,jref=_webout,dslabel=,fmt=Y,engine=DATASTEP,dbg=0 ';
put ')/*/STORE SOURCE*/; ';
put '%put output location=&jref; ';
put '%if &action=OPEN %then %do; ';
put ' options nobomfile; ';
put ' data _null_;file &jref encoding=''utf-8''; ';
put ' put ''{"START_DTTM" : "'' "%sysfunc(datetime(),datetime20.3)" ''"''; ';
put ' run; ';
@@ -117,6 +118,64 @@ data _null_;
put ' %put &sysmacroname: &ds NOT FOUND!!!; ';
put ' %return; ';
put ' %end; ';
put ' %if &fmt=Y %then %do; ';
put ' %put converting every variable to a formatted variable; ';
put ' /* see mp_ds2fmtds.sas for source */ ';
put ' proc contents noprint data=&ds ';
put ' out=_data_(keep=name type length format formatl formatd varnum); ';
put ' run; ';
put ' proc sort; ';
put ' by varnum; ';
put ' run; ';
put ' %local fmtds; ';
put ' %let fmtds=%scan(&syslast,2,.); ';
put ' /* prepare formats and varnames */ ';
put ' data _null_; ';
put ' set &fmtds end=last; ';
put ' name=upcase(name); ';
put ' /* fix formats */ ';
put ' if type=2 or type=6 then do; ';
put ' length fmt $49.; ';
put ' if format='''' then fmt=cats(''$'',length,''.''); ';
put ' else if formatl=0 then fmt=cats(format,''.''); ';
put ' else fmt=cats(format,formatl,''.''); ';
put ' newlen=max(formatl,length); ';
put ' end; ';
put ' else do; ';
put ' if format='''' then fmt=''best.''; ';
put ' else if formatl=0 then fmt=cats(format,''.''); ';
put ' else if formatd=0 then fmt=cats(format,formatl,''.''); ';
put ' else fmt=cats(format,formatl,''.'',formatd); ';
put ' /* needs to be wide, for datetimes etc */ ';
put ' newlen=max(length,formatl,24); ';
put ' end; ';
put ' /* 32 char unique name */ ';
put ' newname=''sasjs''!!substr(cats(put(md5(name),$hex32.)),1,27); ';
put ' ';
put ' call symputx(cats(''name'',_n_),name,''l''); ';
put ' call symputx(cats(''newname'',_n_),newname,''l''); ';
put ' call symputx(cats(''len'',_n_),newlen,''l''); ';
put ' call symputx(cats(''fmt'',_n_),fmt,''l''); ';
put ' call symputx(cats(''type'',_n_),type,''l''); ';
put ' if last then call symputx(''nobs'',_n_,''l''); ';
put ' run; ';
put ' data &fmtds; ';
put ' /* rename on entry */ ';
put ' set &ds(rename=( ';
put ' %local i; ';
put ' %do i=1 %to &nobs; ';
put ' &&name&i=&&newname&i ';
put ' %end; ';
put ' )); ';
put ' %do i=1 %to &nobs; ';
put ' length &&name&i $&&len&i; ';
put ' &&name&i=left(put(&&newname&i,&&fmt&i)); ';
put ' drop &&newname&i; ';
put ' %end; ';
put ' if _error_ then call symputx(''syscc'',1012); ';
put ' run; ';
put ' %let ds=&fmtds; ';
put ' %end; /* &fmt=Y */ ';
put ' data _null_;file &jref mod ; ';
put ' put "["; call symputx(''cols'',0,''l''); ';
put ' proc sort ';
@@ -196,11 +255,11 @@ data _null_;
put '%end; ';
put ' ';
put '%else %if &action=CLOSE %then %do; ';
put ' data _null_;file &jref encoding=''utf-8''; ';
put ' data _null_;file &jref encoding=''utf-8'' mod; ';
put ' put "}"; ';
put ' run; ';
put '%end; ';
put '%mend; ';
put '%mend mp_jsonout; ';
put '%macro mm_webout(action,ds,dslabel=,fref=_webout,fmt=Y); ';
put '%global _webin_file_count _webin_fileref1 _webin_name1 _program _debug ';
put ' sasjs_tables; ';
@@ -237,8 +296,16 @@ data _null_;
put '%else %if &action=OPEN %then %do; ';
put ' /* fix encoding */ ';
put ' OPTIONS NOBOMFILE; ';
put ' ';
put ' /** ';
put ' * check engine type to avoid the below err message: ';
put ' * > Function is only valid for filerefs using the CACHE access method. ';
put ' */ ';
put ' data _null_; ';
put ' rc = stpsrv_header(''Content-type'',"text/html; encoding=utf-8"); ';
put ' set sashelp.vextfl(where=(fileref="_WEBOUT")); ';
put ' if xengine=''STREAM'' then do; ';
put ' rc=stpsrv_header(''Content-type'',"text/html; encoding=utf-8"); ';
put ' end; ';
put ' run; ';
put ' ';
put ' /* setup json */ ';
@@ -252,16 +319,9 @@ data _null_;
put '%end; ';
put ' ';
put '%else %if &action=ARR or &action=OBJ %then %do; ';
put ' %if &sysver=9.4 %then %do; ';
put ' %mp_jsonout(&action,&ds,dslabel=&dslabel,fmt=&fmt ';
put ' ,engine=PROCJSON,dbg=%str(&_debug) ';
put ' ) ';
put ' %end; ';
put ' %else %do; ';
put ' %mp_jsonout(&action,&ds,dslabel=&dslabel,fmt=&fmt ';
put ' ,engine=DATASTEP,dbg=%str(&_debug) ';
put ' ) ';
put ' %end; ';
put ' %mp_jsonout(&action,&ds,dslabel=&dslabel,fmt=&fmt ';
put ' ,engine=DATASTEP,dbg=%str(&_debug) ';
put ' ) ';
put '%end; ';
put '%else %if &action=CLOSE %then %do; ';
put ' %if %str(&_debug) ge 131 %then %do; ';
@@ -277,14 +337,14 @@ data _null_;
put ' i+1; ';
put ' call symputx(''wt''!!left(i),name,''l''); ';
put ' call symputx(''wtcnt'',i,''l''); ';
put ' data _null_; file &fref encoding=''utf-8''; ';
put ' data _null_; file &fref mod encoding=''utf-8''; ';
put ' put ",""WORK"":{"; ';
put ' %do i=1 %to &wtcnt; ';
put ' %let wt=&&wt&i; ';
put ' proc contents noprint data=&wt ';
put ' out=_data_ (keep=name type length format:); ';
put ' run;%let tempds=%scan(&syslast,2,.); ';
put ' data _null_; file &fref encoding=''utf-8''; ';
put ' data _null_; file &fref mod encoding=''utf-8''; ';
put ' dsid=open("WORK.&wt",''is''); ';
put ' nlobs=attrn(dsid,''NLOBS''); ';
put ' nvars=attrn(dsid,''NVARS''); ';
@@ -295,10 +355,10 @@ data _null_;
put ' put '',"nvars":'' nvars; ';
put ' %mp_jsonout(OBJ,&tempds,jref=&fref,dslabel=colattrs,engine=DATASTEP) ';
put ' %mp_jsonout(OBJ,&wt,jref=&fref,dslabel=first10rows,engine=DATASTEP) ';
put ' data _null_; file &fref encoding=''utf-8''; ';
put ' data _null_; file &fref mod encoding=''utf-8''; ';
put ' put "}"; ';
put ' %end; ';
put ' data _null_; file &fref encoding=''utf-8''; ';
put ' data _null_; file &fref mod encoding=''utf-8''; ';
put ' put "}"; ';
put ' run; ';
put ' %end; ';

View File

@@ -32,7 +32,7 @@ data _null_;
call symputx('stpuri',uri,'l');
run;
%if &type ne Document %then %do;
%put WARNING: No Document found at &target;
%put %str(WARN)ING: No Document found at &target;
%return;
%end;

View File

@@ -125,7 +125,7 @@ data _null_;
when ('&#x0d;') rec='0D'x;
when ('&#36;' ) rec='$' ;
when ('&#x09;') rec='09'x;
otherwise putlog "WARNING: missing value for " entity=;
otherwise putlog "%str(WARN)ING: missing value for " entity=;
end;
rc =fput(fileid, substr(rec,1,1));
rc =fwrite(fileid);

View File

@@ -48,7 +48,7 @@
#### &prefix.added
|name:$32.|metaID:$17.|SAStabName:$32.|
|---|---|---|
|||DATA1|
| | |DATA1|
#### &prefix.deleted
|name:$32.|metaID:$17.|SAStabName:$32.|
@@ -59,7 +59,7 @@
|tabName:$32.|tabMetaID:$17.|SAStabName:$32.|metaName:$32.|metaID:$17.|sasname:$32.|metaType:$16.|change:$64.|
|---|---|---|---|---|---|---|---|
|TABLE2|A5XLSNXI.BK0001HN|TABLE2|c|A5XLSNXI.BM000MA9|c|Column|Deleted|
||||d||d|Column|Added|
| | | |d| |d|Column|Added|
#### &prefix.meta
|Label1:$28.|cValue1:$1.|nValue1:D12.3|
@@ -80,9 +80,8 @@
such as dangling metadata, embedded passwords, security issues and more.
@param [in] libname= the metadata name of the library to be compared
@param [out] outlib= The output library in which to store the output tables.
Default=WORK.
@param [out] prefix The prefix for the four tables created. Default=metadiff.
@param [out] outlib=(work) The library in which to store the output tables.
@param [out] prefix=(metadiff) The prefix for the four tables created.
@version 9.3
@author Allan Bowe

View File

@@ -1,20 +1,26 @@
/**
@file
@brief Writes the code of an to an external file, or the log if none provided
@details Get the
@brief Writes the code of an STP to an external file
@details Fetches the SAS code from a Stored Process where the code is stored
in metadata.
usage:
Usage:
%mm_getstpcode(tree=/some/meta/path
,name=someSTP
,outloc=/some/unquoted/filename.ext
)
@param tree= The metadata path of the Stored Process (can also contain name)
@param name= Stored Process name. Leave blank if included above.
@param outloc= full and unquoted path to the desired text file. This will be
overwritten if it already exists. If not provided, the code will be written
to the log.
@param [in] tree= The metadata path of the Stored Process (can also contain
name)
@param [in] name= Stored Process name. Leave blank if included above.
@param [out] outloc= (0) full and unquoted path to the desired text file.
This will be overwritten if it already exists.
@param [out] outref= (0) Fileref to which to write the code.
@param [out] showlog=(NO) Set to YES to print log to the window
<h4> SAS Macros </h4>
@li mf_getuniquefileref.sas
@author Allan Bowe
@@ -23,8 +29,10 @@
%macro mm_getstpcode(
tree=/User Folders/sasdemo/somestp
,name=
,outloc=
,outloc=0
,outref=0
,mDebug=1
,showlog=NO
);
%local mD;
@@ -92,14 +100,18 @@ data _null_;
stop;
%local outeng;
%if %length(&outloc)=0 %then %let outeng=TEMP;
%if "&outloc"="0" %then %let outeng=TEMP;
%else %let outeng="&outloc";
%local fref;
%if &outref=0 %then %let fref=%mf_getuniquefileref();
%else %let fref=&outref;
/* read the content, byte by byte, resolving escaped chars */
filename __outdoc &outeng lrecl=100000;
filename &fref &outeng lrecl=100000;
data _null_;
length filein 8 fileid 8;
filein = fopen("__getdoc","I",1,"B");
fileid = fopen("__outdoc","O",1,"B");
fileid = fopen("&fref","O",1,"B");
rec = "20"x;
length entity $6;
do while(fread(filein)=0);
@@ -140,9 +152,9 @@ data _null_;
rc=fclose(fileid);
run;
%if &outeng=TEMP %then %do;
%if &showlog=YES %then %do;
data _null_;
infile __outdoc lrecl=32767 end=last;
infile &fref lrecl=32767 end=last;
input;
if _n_=1 then putlog '>>stpcodeBEGIN<<';
putlog _infile_;
@@ -151,6 +163,8 @@ run;
%end;
filename __getdoc clear;
filename __outdoc clear;
%if &outref=0 %then %do;
filename &fref clear;
%end;
%mend;
%mend mm_getstpcode;

View File

@@ -99,7 +99,7 @@ run;
when ('&#x0d;') rec='0D'x;
when ('&#36;' ) rec='$' ;
when ('&#x09;') rec='09'x;
otherwise putlog "WARNING: missing value for " entity=;
otherwise putlog "%str(WARN)ING: missing value for " entity=;
end;
rc =fput(fileid, substr(rec,1,1));
rc =fwrite(fileid);

View File

@@ -70,7 +70,7 @@ data _null_;
run;
%if &appuri=stopifempty %then %do;
%put WARNING: &app.(Application) not found!;
%put %str(WARN)ING: &app.(Application) not found!;
%return;
%end;

View File

@@ -58,12 +58,12 @@ data _null_;
run;
%if &tsuri=stopifempty %then %do;
%put WARNING: &path/&name.(Document) not found!;
%put %str(WARN)ING: &path/&name.(Document) not found!;
%return;
%end;
%if %length(&text)<2 %then %do;
%put WARNING: No text supplied!!;
%put %str(WARN)ING: No text supplied!!;
%return;
%end;

View File

@@ -34,7 +34,7 @@ data _null_;
call symputx('stpuri',uri,'l');
run;
%if &cmtype ne ClassifierMap %then %do;
%put WARNING: No Stored Process found at &target;
%put %str(WARN)ING: No Stored Process found at &target;
%return;
%end;

View File

@@ -68,12 +68,12 @@ data _null_;
run;
%if &tsuri=stopifempty %then %do;
%put WARNING: &stp.(StoredProcess) not found!;
%put %str(WARN)ING: &stp.(StoredProcess) not found!;
%return;
%end;
%if %length(&stpcode)<2 %then %do;
%put WARNING: No SAS code supplied!!;
%put %str(WARN)ING: No SAS code supplied!!;
%return;
%end;

View File

@@ -68,8 +68,16 @@
%else %if &action=OPEN %then %do;
/* fix encoding */
OPTIONS NOBOMFILE;
/**
* check engine type to avoid the below err message:
* > Function is only valid for filerefs using the CACHE access method.
*/
data _null_;
rc = stpsrv_header('Content-type',"text/html; encoding=utf-8");
set sashelp.vextfl(where=(fileref="_WEBOUT"));
if xengine='STREAM' then do;
rc=stpsrv_header('Content-type',"text/html; encoding=utf-8");
end;
run;
/* setup json */
@@ -83,16 +91,9 @@
%end;
%else %if &action=ARR or &action=OBJ %then %do;
%if &sysver=9.4 %then %do;
%mp_jsonout(&action,&ds,dslabel=&dslabel,fmt=&fmt
,engine=PROCJSON,dbg=%str(&_debug)
)
%end;
%else %do;
%mp_jsonout(&action,&ds,dslabel=&dslabel,fmt=&fmt
,engine=DATASTEP,dbg=%str(&_debug)
)
%end;
%mp_jsonout(&action,&ds,dslabel=&dslabel,fmt=&fmt
,engine=DATASTEP,dbg=%str(&_debug)
)
%end;
%else %if &action=CLOSE %then %do;
%if %str(&_debug) ge 131 %then %do;
@@ -108,14 +109,14 @@
i+1;
call symputx('wt'!!left(i),name,'l');
call symputx('wtcnt',i,'l');
data _null_; file &fref encoding='utf-8';
data _null_; file &fref mod encoding='utf-8';
put ",""WORK"":{";
%do i=1 %to &wtcnt;
%let wt=&&wt&i;
proc contents noprint data=&wt
out=_data_ (keep=name type length format:);
run;%let tempds=%scan(&syslast,2,.);
data _null_; file &fref encoding='utf-8';
data _null_; file &fref mod encoding='utf-8';
dsid=open("WORK.&wt",'is');
nlobs=attrn(dsid,'NLOBS');
nvars=attrn(dsid,'NVARS');
@@ -126,10 +127,10 @@
put ',"nvars":' nvars;
%mp_jsonout(OBJ,&tempds,jref=&fref,dslabel=colattrs,engine=DATASTEP)
%mp_jsonout(OBJ,&wt,jref=&fref,dslabel=first10rows,engine=DATASTEP)
data _null_; file &fref encoding='utf-8';
data _null_; file &fref mod encoding='utf-8';
put "}";
%end;
data _null_; file &fref encoding='utf-8';
data _null_; file &fref mod encoding='utf-8';
put "}";
run;
%end;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 146 KiB

View File

@@ -1,7 +1,13 @@
{
"$schema": "https://cli.sasjs.io/sasjsconfig-schema.json",
"macroFolders": ["base", "meta", "metax", "viya", "lua"],
"docConfig":{
"macroFolders": [
"base",
"meta",
"metax",
"viya",
"lua"
],
"docConfig": {
"displayMacroCore": false,
"enableLineage": false,
"doxyContent": {
@@ -9,5 +15,34 @@
"logo": "Macro_core_website_1.png",
"readMe": "../../README.md"
}
}
}
},
"defaultTarget": "viya",
"targets": [
{
"name": "viya",
"serverUrl": "https://sas.analytium.co.uk",
"serverType": "SASVIYA",
"appLoc": "/Public/temp/macrocore",
"deployConfig": {
"deployServicePack": true
},
"macroFolders": [
"base",
"meta",
"metax",
"viya",
"lua",
"tests/base",
"tests/viya"
],
"contextName": "SAS Job Execution compute context",
"testConfig": {
"initProgram": "tests/testinit.sas",
"termProgram": "tests/testterm.sas",
"macroVars": {
"mcTestAppLoc": "/Public/temp/macrocore"
}
}
}
]
}

View File

@@ -0,0 +1,60 @@
/**
@file
@brief Testing mf_getvarlist macro
<h4> SAS Macros </h4>
@li mf_getvarlist.sas
**/
%let test1=%mf_getvarlist(sashelp.class);
%let test2=%mf_getvarlist(sashelp.class,dlm=X);
%let test3=%mf_getvarlist(sashelp.class,dlm=%str(,),quote=double);
%let test4=%mf_getvarlist(sashelp.class,typefilter=N);
%let test5=%mf_getvarlist(sashelp.class,typefilter=C);
data work.test_results;
length test_description $256 test_result $4 test_comments base result $256;
test_description="Basic test";
base=symget('test1');
result='Name Sex Age Height Weight';
if base=result then test_result='PASS';
else test_result='FAIL';
test_comments="Comparing "!!trim(base)!!' vs '!!trim(result);
output;
test_description="DLM test";
base=symget('test2');
result='NameXSexXAgeXHeightXWeight';
if base=result then test_result='PASS';
else test_result='FAIL';
test_comments="Comparing "!!trim(base)!!' vs '!!trim(result);
output;
test_description="DLM + quote test";
base=symget('test3');
result='"Name","Sex","Age","Height","Weight"';
if base=result then test_result='PASS';
else test_result='FAIL';
test_comments="Comparing "!!trim(base)!!' vs '!!trim(result);
output;
test_description="Numeric Filter";
base=symget('test4');
result='Age Height Weight';
if base=result then test_result='PASS';
else test_result='FAIL';
test_comments="Comparing "!!trim(base)!!' vs '!!trim(result);
output;
test_description="Char Filter";
base=symget('test5');
result='Name Sex';
if base=result then test_result='PASS';
else test_result='FAIL';
test_comments="Comparing "!!trim(base)!!' vs '!!trim(result);
output;
drop base result;
run;

View File

@@ -0,0 +1,36 @@
/**
@file
@brief Testing mp_abort macro
@details This is an unfixed problem with mp_abort when using the
'unclosed macro' technique. This is only relevant for 9.4m3 environments,
which can suffer from hung multibridge sessions from %abort and endsas.
The issue is that when called within a macro, within a %include, AND that
macro contains subsequent logic, the service does not end cleanly - rather,
we see:
ERROR: %EVAL function has no expression to evaluate, or %IF statement has no condition.
ERROR: The macro TEST will stop executing.
We are not able to test this without a 9.4m3 environment, it is marked as
nofix.
<h4> SAS Macros </h4>
@li mp_abort.sas
@li mp_assert.sas
**/
%macro test();
filename blah temp;
data _null_;
file blah;
put '%mp_abort();';
run;
%inc blah;
%if 1=1 %then %put Houston - we have a problem here;
%mend test;
%test()

View File

@@ -0,0 +1,32 @@
/**
@file
@brief Testing mp_assertcolvals macro
<h4> SAS Macros </h4>
@li mp_assertcolvals.sas
**/
data work.checkds;
do checkval='Jane','James','Jill';
output;
end;
run;
%mp_assertcolvals(sashelp.class.name,
checkvals=work.checkds.checkval,
desc=At least one value has a match,
test=ANYVAL
)
data work.check;
do val='M','F';
output;
end;
run;
%mp_assertcolvals(sashelp.class.sex,
checkvals=work.check.val,
desc=All values have a match,
test=ALLVALS
)

View File

@@ -0,0 +1,60 @@
/**
@file
@brief Testing mp_assertdsobs.sas macro
<h4> SAS Macros </h4>
@li mp_assertdsobs.sas
@li mp_assertcolvals.sas
**/
data work.somedata;
do x=1 to 15;
output;
end;
run;
%mp_assertdsobs(work.somedata,
test=ATLEAST 15,
outds=work.test_the_test
)
%mp_assertdsobs(work.somedata,
test=ATMOST 15,
outds=work.test_the_test
)
data work.check;
val='PASS';
run;
%mp_assertcolvals(work.test_the_test.test_result,
checkvals=work.check.val,
desc=Testing ATLEAST / ATMOST for passes,
test=ALLVALS
)
%mp_assertdsobs(work.somedata,
test=ATLEAST 16,
outds=work.test_the_test2
)
%mp_assertdsobs(work.somedata,
test=ATMOST 14,
outds=work.test_the_test2
)
data _null_;
set work.test_the_test2;
putlog (_all_)(=);
run;
data work.check2;
val='FAIL';
run;
%mp_assertcolvals(work.test_the_test2.test_result,
checkvals=work.check2.val,
desc=Testing ATLEAST / ATMOST for failures,
test=ALLVALS
)

View File

@@ -0,0 +1,28 @@
/**
@file
@brief Testing mp_ds2fmtds.sas macro
<h4> SAS Macros </h4>
@li mp_ds2fmtds.sas
@li mp_assert.sas
**/
proc sql;
create table test as select * from dictionary.tables where libname='SASHELP';
filename inc temp;
data _null_;
set work.test;
file inc;
line=cats('%mp_ds2fmtds(sashelp.',memname,',',memname,')');
put line;
run;
%inc inc;
%mp_assert(
iftrue=(&syscc=0),
desc=Checking tables were created successfully,
outds=work.test_results
)

View File

@@ -0,0 +1,148 @@
/**
@file
@brief Testing mp_filtercheck macro
<h4> SAS Macros </h4>
@li mp_filtercheck.sas
@li mp_assertdsobs.sas
@li mp_assert.sas
**/
/* valid filter */
data work.inds;
infile datalines4 dsd;
input GROUP_LOGIC:$3. SUBGROUP_LOGIC:$3. SUBGROUP_ID:8. VARIABLE_NM:$32.
OPERATOR_NM:$10. RAW_VALUE:$4000.;
datalines4;
AND,AND,1,AGE,=,12
AND,AND,1,SEX,<=,"'M'"
AND,OR,2,Name,NOT IN,"('Jane','Alfred')"
AND,OR,2,Weight,>=,77.7
AND,OR,2,Weight,NE,77.7
;;;;
run;
%mp_filtercheck(work.inds,
targetds=sashelp.class,
outds=work.badrecords,
abort=NO
)
%let syscc=0;
%mp_assertdsobs(work.badrecords,
desc=Valid filter query,
test=EMPTY,
outds=work.test_results
)
/* invalid column */
data work.inds;
infile datalines4 dsd;
input GROUP_LOGIC:$3. SUBGROUP_LOGIC:$3. SUBGROUP_ID:8. VARIABLE_NM:$32.
OPERATOR_NM:$10. RAW_VALUE:$4000.;
datalines4;
AND,AND,1,invalid,=,12
AND,AND,1,SEX,<=,"'M'"
AND,OR,2,Name,NOT IN,"('Jane','Alfred')"
AND,OR,2,Weight,>=,7
;;;;
run;
%mp_filtercheck(work.inds,
targetds=sashelp.class,
outds=work.badrecords,
abort=NO
)
%let syscc=0;
%mp_assertdsobs(work.badrecords,
desc=Invalid column name,
test=HASOBS,
outds=work.test_results
)
/* invalid raw value */
data work.inds;
infile datalines4 dsd;
input GROUP_LOGIC:$3. SUBGROUP_LOGIC:$3. SUBGROUP_ID:8. VARIABLE_NM:$32.
OPERATOR_NM:$10. RAW_VALUE:$4000.;
datalines4;
AND,OR,2,Name,NOT IN,"(''''Jane','Alfred')"
;;;;
run;
%mp_filtercheck(work.inds,
targetds=sashelp.class,
outds=work.badrecords,
abort=NO
)
%let syscc=0;
%mp_assertdsobs(work.badrecords,
desc=Invalid raw value,
test=HASOBS,
outds=work.test_results
)
/* Code injection - column name */
data work.inds;
infile datalines4 dsd;
input GROUP_LOGIC:$3. SUBGROUP_LOGIC:$3. SUBGROUP_ID:8. VARIABLE_NM:$32.
OPERATOR_NM:$10. RAW_VALUE:$4000.;
datalines4;
AND,AND,1,%abort,=,12
AND,OR,2,Weight,>=,7
;;;;
run;
%mp_filtercheck(work.inds,
targetds=sashelp.class,
outds=work.badrecords,
abort=NO
)
%let syscc=0;
%mp_assertdsobs(work.badrecords,
desc=Code injection - column name,
test=HASOBS,
outds=work.test_results
)
/* Code injection - raw values*/
data work.inds;
infile datalines4 dsd;
input GROUP_LOGIC:$3. SUBGROUP_LOGIC:$3. SUBGROUP_ID:8. VARIABLE_NM:$32.
OPERATOR_NM:$10. RAW_VALUE:$4000.;
datalines4;
AND,AND,1,age,=,;;%abort
;;;;
run;
%mp_filtercheck(work.inds,
targetds=sashelp.class,
outds=work.badrecords,
abort=NO
)
%let syscc=0;
%mp_assertdsobs(work.badrecords,
desc=Code injection - raw value abort,
test=HASOBS,
outds=work.test_results
)
/* Supply variables with incorrect types */
data work.inds;
infile datalines4 dsd;
input GROUP_LOGIC:$3. SUBGROUP_LOGIC:$3. SUBGROUP_ID:8. VARIABLE_NM:$32.
OPERATOR_NM:$10. RAW_VALUE:8;
datalines4;
AND,AND,1,age,=,0
;;;;
run;
%let syscc=0;
%mp_filtercheck(work.inds,
targetds=sashelp.class,
outds=work.badrecords,
abort=NO
)
%mp_assert(iftrue=(&syscc=42),
desc=Throw error if RAW_VALUE is incorrect,
outds=work.test_results
)
%let syscc=0;

View File

@@ -0,0 +1,122 @@
/**
@file
@brief Testing mp_filtergenerate macro
<h4> SAS Macros </h4>
@li mp_filtergenerate.sas
@li mp_filtercheck.sas
@li mp_assertdsobs.sas
**/
options source2;
/* valid filter */
data work.inds;
infile datalines4 dsd;
input GROUP_LOGIC:$3. SUBGROUP_LOGIC:$3. SUBGROUP_ID:8. VARIABLE_NM:$32.
OPERATOR_NM:$10. RAW_VALUE:$4000.;
datalines4;
AND,AND,1,AGE,>,5
AND,AND,1,SEX,NE,"'M'"
AND,OR,2,Name,NOT IN,"('Jane','Janet')"
AND,OR,2,Weight,>=,84.6
;;;;
run;
%mp_filtercheck(work.inds,targetds=sashelp.class)
%mp_filtergenerate(work.inds,outref=myfilter)
data work.test;
set sashelp.class;
where %inc myfilter;;
run;
%mp_assertdsobs(work.test,
desc=Valid filter,
test=EQUALS 8,
outds=work.test_results
)
/* empty filter (return all records) */
data work.inds;
infile datalines4 dsd;
input GROUP_LOGIC:$3. SUBGROUP_LOGIC:$3. SUBGROUP_ID:8. VARIABLE_NM:$32.
OPERATOR_NM:$10. RAW_VALUE:$4000.;
datalines4;
;;;;
run;
%mp_filtercheck(work.inds,targetds=sashelp.class)
%mp_filtergenerate(work.inds,outref=myfilter)
data work.test;
set sashelp.class;
where %inc myfilter;;
run;
%mp_assertdsobs(work.test,
desc=Empty filter (return all records) ,
test=EQUALS 19,
outds=work.test_results
)
/* single line filter */
data work.inds;
infile datalines4 dsd;
input GROUP_LOGIC:$3. SUBGROUP_LOGIC:$3. SUBGROUP_ID:8. VARIABLE_NM:$32.
OPERATOR_NM:$10. RAW_VALUE:$4000.;
datalines4;
AND,OR,2,Name,IN,"('Jane','Janet')"
;;;;
run;
%mp_filtercheck(work.inds,targetds=sashelp.class)
%mp_filtergenerate(work.inds,outref=myfilter)
data work.test;
set sashelp.class;
where %inc myfilter;;
run;
%mp_assertdsobs(work.test,
desc=Single line filter ,
test=EQUALS 2,
outds=work.test_results
)
/* single line 2 group filter */
data work.inds;
infile datalines4 dsd;
input GROUP_LOGIC:$3. SUBGROUP_LOGIC:$3. SUBGROUP_ID:8. VARIABLE_NM:$32.
OPERATOR_NM:$10. RAW_VALUE:$4000.;
datalines4;
OR,OR,2,Name,IN,"('Jane','Janet')"
OR,OR,3,Name,IN,"('James')"
;;;;
run;
%mp_filtercheck(work.inds,targetds=sashelp.class)
%mp_filtergenerate(work.inds,outref=myfilter)
data work.test;
set sashelp.class;
where %inc myfilter;;
run;
%mp_assertdsobs(work.test,
desc=Single line 2 group filter ,
test=EQUALS 3,
outds=work.test_results
)
/* filter with nothing returned */
data work.inds;
infile datalines4 dsd;
input GROUP_LOGIC:$3. SUBGROUP_LOGIC:$3. SUBGROUP_ID:8. VARIABLE_NM:$32.
OPERATOR_NM:$10. RAW_VALUE:$4000.;
datalines4;
AND,OR,2,Name,IN,"('Jane','Janet')"
AND,OR,3,Name,IN,"('James')"
;;;;
run;
%mp_filtercheck(work.inds,targetds=sashelp.class)
%mp_filtergenerate(work.inds,outref=myfilter)
data work.test;
set sashelp.class;
where %inc myfilter;;
run;
%mp_assertdsobs(work.test,
desc=Filter with nothing returned,
test=EQUALS 0,
outds=work.test_results
)

View File

@@ -0,0 +1,68 @@
/**
@file
@brief Testing mp_filtervalidate macro
<h4> SAS Macros </h4>
@li mp_filtergenerate.sas
@li mp_filtervalidate.sas
@li mp_assertdsobs.sas
**/
/* valid filter */
data work.inds;
infile datalines4 dsd;
input GROUP_LOGIC:$3. SUBGROUP_LOGIC:$3. SUBGROUP_ID:8. VARIABLE_NM:$32.
OPERATOR_NM:$10. RAW_VALUE:$4000.;
datalines4;
AND,AND,1,AGE,>,5
AND,AND,1,SEX,NE,"'M'"
AND,OR,2,Name,NOT IN,"('Jane','Janet')"
AND,OR,2,Weight,>=,84.6
;;;;
run;
%mp_filtergenerate(work.inds,outref=myfilter)
%mp_filtervalidate(myfilter,sashelp.class,outds=work.results,abort=NO)
%mp_assertdsobs(work.results,
desc=Valid filter,
test=EMPTY,
outds=work.test_results
)
/* empty filter (return all records) */
data work.inds;
infile datalines4 dsd;
input GROUP_LOGIC:$3. SUBGROUP_LOGIC:$3. SUBGROUP_ID:8. VARIABLE_NM:$32.
OPERATOR_NM:$10. RAW_VALUE:$4000.;
datalines4;
;;;;
run;
%mp_filtergenerate(work.inds,outref=myfilter)
%mp_filtervalidate(myfilter,sashelp.class,outds=work.results,abort=NO)
%mp_assertdsobs(work.results,
desc=Valid filter,
test=EMPTY,
outds=work.test_results
)
/* invalid filter*/
data work.inds;
infile datalines4 dsd;
input GROUP_LOGIC:$3. SUBGROUP_LOGIC:$3. SUBGROUP_ID:8. VARIABLE_NM:$32.
OPERATOR_NM:$10. RAW_VALUE:$4000.;
datalines4;
AND,AND,1,SEX,NE,2
;;;;
run;
%mp_filtergenerate(work.inds,outref=myfilter)
%mp_filtervalidate(myfilter,sashelp.class,outds=work.results,abort=NO)
%let syscc=0;
%mp_assertdsobs(work.results,
desc=Valid filter,
test=EQUALS 1,
outds=work.test_results
)

View File

@@ -0,0 +1,42 @@
/**
@file
@brief Testing mp_jsonout.sas macro
<h4> SAS Macros </h4>
@li mp_jsonout.sas
@li mp_assert.sas
**/
filename webref temp;
data demo;
dtval=date();
format dtval date9.;
compare=put(date(),date9.);
call symputx('compare',compare);
run;
%mp_jsonout(OPEN,jref=webref)
%mp_jsonout(OBJ,demo,jref=webref,fmt=Y)
%mp_jsonout(CLOSE,jref=webref)
data _null_;
infile webref;
input;
putlog _infile_;
run;
libname web JSON fileref=webref;
%let dtval=0;
data work.test;
set web.demo;
call symputx('dtval',dtval);
run;
%mp_assert(
iftrue=(&dtval=&compare),
desc=Checking tables were created successfully,
outds=work.test_results
)

View File

@@ -0,0 +1,50 @@
/**
@file
@brief Testing mp_searchcols.sas
<h4> SAS Macros </h4>
@li mp_searchcols.sas
@li mp_assertdsobs.sas
**/
/** Test 1 - full col match */
data example1;
var1=1;
var2=2;
var3=3;
data example2;
var1=1;
var2=2;
data example3;
var2=2;
var3=3;
data example4;
matchmehere=1;
data example5;
hereyoucan_matchme_also=1;
data example6;
do_not_forget_me=1;
data example7;
we_shall_not_forget=1;
run;
%mp_searchcols(libs=work,cols=var1 var2,outds=testme)
%mp_assertdsobs(work.testme,
desc=Test1 - check exact variables are found,
test=EQUALS 3,
outds=work.test_results
)
/* test 2 - wildcard match */
%mp_searchcols(libs=work,cols=matchme forget,match=WILD, outds=testme2)
%mp_assertdsobs(work.testme2,
desc=Test1 - check fuzzy matches are found,
test=EQUALS 4,
outds=work.test_results
)

View File

@@ -0,0 +1,62 @@
/**
@file
@brief Testing mp_validatecol.sas macro
<h4> SAS Macros </h4>
@li mp_assertdsobs.sas
@li mp_validatecol.sas
**/
/**
* Test 1 - LIBDS
*/
data test1;
infile datalines4 dsd;
input;
libds=_infile_;
%mp_validatecol(libds,LIBDS,is_libds)
if is_libds=1;
datalines4;
some.libname
!lib.blah
%abort
definite.ok
not.ok!
nineletrs._
;;;;
run;
%mp_assertdsobs(work.test1,
desc=Testing LIBDS,
test=EQUALS 2,
outds=work.test_results
)
/**
* Test 2 - ISNUM
*/
data test2;
infile datalines4 dsd;
input;
infile=_infile_;
%mp_validatecol(infile,ISNUM,is_numeric)
if is_numeric=1;
datalines4;
1
0001
1e6
-44
above are good
the rest are bad
%abort
1&somethingverybad.
&
+-1
;;;;
run;
%mp_assertdsobs(work.test2,
desc=Test2 - ISNUM,
test=EQUALS 4,
outds=work.test_results
)

8
tests/testinit.sas Normal file
View File

@@ -0,0 +1,8 @@
/**
@file
@brief init file for tests
**/
/* location in metadata or SAS Drive for temporary files */
%let mcTestAppLoc=/Public/temp/macrocore;

9
tests/testterm.sas Normal file
View File

@@ -0,0 +1,9 @@
/**
@file
@brief term file for tests
**/
%webout(OPEN)
%webout(OBJ, TEST_RESULTS)
%webout(CLOSE)

View File

@@ -0,0 +1,45 @@
/**
@file
@brief Testing mv_createwebservice macro
<h4> SAS Macros </h4>
@li mv_createwebservice.sas
@li mv_getjobcode.sas
**/
/**
* Test Case 1
* Send special char in a service
*/
filename testref temp;
data _null_;
file testref;
put '01'x;
run;
%put TEST1: creating web service;
%mv_createwebservice(
path=&mcTestAppLoc/temp/macros,
name=mv_createwebservice,
code=testref
)
%put TEST1: fetching web service code;
%mv_getjobcode(
path=&mcTestAppLoc/temp/macros,
name=mv_createwebservice,
outref=compare
)
%put TEST1: checking web service code;
data work.test_results;
length test_description $256 test_result $4 test_comments $256;
infile compare end=eof;
input;
if eof then do;
if _infile_='01'x then test_result='PASS';
else test_result='FAIL';
test_description="Creating web service with invisible character";
output;
stop;
end;
run;

View File

@@ -0,0 +1,49 @@
/**
@file
@brief Testing mv_getjobcode macro
<h4> SAS Macros </h4>
@li mp_assert.sas
@li mv_createjob.sas
@li mv_getjobcode.sas
**/
/**
* Test Case 1
*/
/* write some code to a job */
%let incode=%str(data test; set sashelp.class;run;);
filename testref temp;
data _null_;
file testref;
put "&incode";
run;
%mv_createjob(
code=testref,
path=&mcTestAppLoc/services/temp,
name=some_job
)
/* now get the code back */
%mv_getjobcode(
path=&mcTestAppLoc/services/temp,
name=some_job,
outref=mycode
)
%let diditexist=NO;
data work.test1;
infile mycode;
input;
putlog _infile_;
line=_infile_;
check=symget('incode');
if _infile_=symget('incode') then call symputx('diditexist','YES');
run;
%mp_assert(
iftrue=(&diditexist=NO),
desc=Check if the code that was sent was successfully retrieved
)

View File

@@ -0,0 +1,70 @@
/**
@file
@brief Testing mv_createwebservice macro
<h4> SAS Macros </h4>
@li mp_assert.sas
@li mv_createjob.sas
@li mv_jobexecute.sas
@li mv_jobwaitfor.sas
@li mv_getjoblog.sas
**/
/**
* Test Case 1
*/
/* create a service */
filename testref temp;
data _null_;
file testref;
put 'data;run;';
put 'endsas;';
run;
%mv_createjob(
path=&mcTestAppLoc/jobs/temp,
code=testref,
name=testjob
)
%* Execute it;
%mv_jobexecute(
path=&mcTestAppLoc/jobs/temp,
name=testjob,
outds=work.info
)
%* Wait for it to finish;
data work.info;
set work.info;
where method='GET' and rel='state';
run;
%mv_jobwaitfor(ALL,inds=work.info,outds=work.jobstates)
%* and grab the uri;
data _null_;
set work.jobstates;
call symputx('uri',uri);
run;
%* Finally, fetch the log;
%mv_getjoblog(uri=%str(&uri),outref=mylog)
data _null_;
infile mylog end=eof;
input;
putlog _infile_;
retain found 0;
if index(_infile_,'endsas;') then do;
found=1;
call symputx('found',found);
end;
else if eof and found ne 1 then call symputx('found',0);
run;
%mp_assert(
iftrue=(%str(&found)=1),
desc=Check if the log was still fetched even though endsas was submitted
)

View File

@@ -0,0 +1,74 @@
/**
@file
@brief Testing mv_createwebservice macro
<h4> SAS Macros </h4>
@li mp_assertdsobs.sas
@li mv_createwebservice.sas
@li mv_getjobresult.sas
@li mv_jobflow.sas
**/
/**
* Test Case 1
*/
/* create a service */
filename testref temp;
data _null_;
file testref;
put 'data test; set sashelp.class;run;';
put '%webout(OPEN)';
put '%webout(OBJ,test)';
put '%webout(CLOSE)';
run;
%mv_createwebservice(
path=&mcTestAppLoc/services/temp,
code=testref,
name=testsvc
)
/* trigger and wait for it to finish */
data work.inputjobs;
_program="&mcTestAppLoc/services/temp/testsvc";
run;
%mv_jobflow(inds=work.inputjobs
,maxconcurrency=4
,outds=work.results
,outref=myjoblog
)
/* stream the log */
data _null_;
infile myjoblog;
input;
put _infile_;
run;
/* fetch the uri */
data _null_;
set work.results;
call symputx('uri',uri);
put (_all_)(=);
run;
/* now get the results */
%mv_getjobresult(uri=&uri
,result=WEBOUT_JSON
,outref=myweb
,outlib=myweblib
)
data _null_;
infile myweb;
input;
putlog _infile_;
run;
data work.out;
set myweblib.test;
put (_all_)(=);
run;
%mp_assertdsobs(work.out,
desc=Test1 - 19 obs from sashelp.class in service result,
test=EQUALS 19,
outds=work.test_results
)

View File

@@ -0,0 +1,78 @@
/**
@file
@brief Testing mv_jobflow macro
@details One of the remote jobs aborts with syscc>0 - test to
make sure this comes back to the calling session
<h4> SAS Macros </h4>
@li mp_assert.sas
@li mv_createjob.sas
@li mv_jobflow.sas
**/
/**
* Test Case 1
*/
filename testprog temp;
data _null_;
file testprog;
put '%put this is job: &_program;'
/ '%put this was run in flow &flow_id;'
/ 'data ;'
/ ' rval=rand("uniform");'
/ ' rand=rval*&macrovar1;'
/ ' do x=1 to rand;'
/ ' y=rand*&macrovar2;'
/ ' if (rval>0.50) then abort;'
/ ' else output;'
/ ' end;'
/ 'run;'
;
run;
%mv_createjob(path=/Public/temp,name=demo1,code=testprog)
%mv_createjob(path=/Public/temp,name=demo2,code=testprog)
data work.inputjobs;
_contextName='SAS Job Execution compute context';
do flow_id=1 to 2;
do i=1 to 4;
_program='/Public/temp/demo1';
macrovar1=10*i;
macrovar2=4*i;
output;
i+1;
_program='/Public/temp/demo2';
macrovar1=40*i;
macrovar2=44*i;
output;
end;
end;
run;
* Trigger the flow ;
%put NOTE: &=syscc;
%mv_jobflow(inds=work.inputjobs
,maxconcurrency=2
,outds=work.results
,outref=myjoblog
,raise_err=1
,mdebug=1
)
%put NOTE: &=syscc;
data _null_;
infile myjoblog;
input; put _infile_;
run;
%mp_assert(
iftrue=(&syscc ne 0),
desc=Check that non zero return code is returned if called job fails
)

View File

@@ -0,0 +1,74 @@
/**
@file
@brief Testing mv_jobflow macro
@details All jobs complete successfully with syscc = 0 - test to
make sure this comes back to the calling session
<h4> SAS Macros </h4>
@li mp_assert.sas
@li mv_createjob.sas
@li mv_jobflow.sas
**/
/**
* Test Case 1
*/
filename testprog temp;
data _null_;
file testprog;
put '%put this is job: &_program;'
/ '%put this was run in flow &flow_id;'
/ 'data ;'
/ ' rval=rand("uniform");'
/ ' rand=rval*&macrovar1;'
/ ' do x=1 to rand;'
/ ' y=rand*&macrovar2;'
/ ' output;'
/ ' end;'
/ 'run;'
;
run;
%mv_createjob(path=/Public/temp,name=demo1,code=testprog)
%mv_createjob(path=/Public/temp,name=demo2,code=testprog)
data work.inputjobs;
_contextName='SAS Job Execution compute context';
do flow_id=1 to 2;
do i=1 to 4;
_program='/Public/temp/demo1';
macrovar1=10*i;
macrovar2=4*i;
output;
i+1;
_program='/Public/temp/demo2';
macrovar1=40*i;
macrovar2=44*i;
output;
end;
end;
run;
* Trigger the flow ;
%put NOTE: &=syscc;
%mv_jobflow(inds=work.inputjobs
,maxconcurrency=2
,outds=work.results
,outref=myjoblog
,raise_err=1
,mdebug=1
)
%put NOTE: &=syscc;
data _null_;
infile myjoblog;
input; put _infile_;
run;
%mp_assert(
iftrue=(&syscc eq 0),
desc=Check that a zero return code is returned if no called job fails
)

View File

@@ -0,0 +1,36 @@
/**
@file
@brief Testing mv_registerclient.sas macro
@details Tests for successful registration. For this to work, the test
account must be an admin.
<h4> SAS Macros </h4>
@li mf_getuniquename.sas
@li mp_assertcolvals.sas
@li mv_registerclient.sas
**/
/**
* Test Case 1
*/
%let id=%mf_getuniquename();
%let sec=%mf_getuniquename();
%mv_registerclient(client_id=&id,client_secret=&sec, outds=testds)
data work.checkds;
id="&id";
sec="&sec";
run;
%mp_assertcolvals(work.testds.client_id,
checkvals=work.checkds.id,
desc=Checking client id was created
test=ALLVALS
)
%mp_assertcolvals(work.testds.client_secret,
checkvals=work.checkds.sec,
desc=Checking client secret was created
test=ALLVALS
)

View File

@@ -0,0 +1,43 @@
/**
@file
@brief Testing mv_registerclient.sas macro
@details Tests for unsuccessful registration. To do this, overrides are
applied for the mf_loc.sas and mp_abort.sas macros.
<h4> SAS Macros </h4>
@li mp_assert.sas
@li mv_registerclient.sas
**/
/**
* Test Case
*/
%macro mf_loc(param);
/does/not/exist
%mend mf_loc;
%macro mp_abort(iftrue=,mac=mp_abort.sas, type=, msg=);
%if not(%eval(%unquote(&iftrue))) %then %return;
%put %substr(&msg,1,16);
%mp_assert(
iftrue=("%substr(&msg,1,16)"="Unable to access"),
desc=Check that abort happens when consul token is unavailable
)
%webout(OPEN)
%webout(OBJ, TEST_RESULTS)
%webout(CLOSE)
%let syscc=0;
data _null_;
abort cancel nolist;
run;
%mend mp_abort;
%mv_registerclient( outds=testds)
%mp_assert(
iftrue=(0=1),
desc=Check that abort happens when consul token is unavailable
)

View File

@@ -9,8 +9,8 @@
@param path= The full path of the folder to be created
@param access_token_var= The global macro variable to contain the access token
@param grant_type= valid values are "password" or "authorization_code" (unquoted).
The default is authorization_code.
@param grant_type= (authorization_code) Valid values are "password" or
"authorization_code" (unquoted).
@version VIYA V.03.04
@@ -39,7 +39,6 @@
%let &access_token_var=;
%end;
%put &sysmacroname: grant_type=&grant_type;
%mp_abort(iftrue=(&grant_type ne authorization_code and &grant_type ne password
and &grant_type ne sas_services
)
@@ -85,12 +84,15 @@ options noquotelenmax;
%local libref1;
%let libref1=%mf_getuniquelibref();
libname &libref1 JSON fileref=&fname1;
%mp_abort(iftrue=(&SYS_PROCHTTP_STATUS_CODE ne 200 and &SYS_PROCHTTP_STATUS_CODE ne 404)
%mp_abort(
iftrue=(
&SYS_PROCHTTP_STATUS_CODE ne 200 and &SYS_PROCHTTP_STATUS_CODE ne 404
)
,mac=&sysmacroname
,msg=%str(&SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE)
)
%if &SYS_PROCHTTP_STATUS_CODE=200 %then %do;
%put &sysmacroname &newpath exists so grab the follow on link ;
%*put &sysmacroname &newpath exists so grab the follow on link ;
data _null_;
set &libref1..links;
if rel='createChild' then

View File

@@ -72,7 +72,6 @@ https://go.documentation.sas.com/?docsetId=calcontexts&docsetTarget=n1hjn8eobk5p
%let oauth_bearer=oauth_bearer=sas_services;
%let &access_token_var=;
%end;
%put &sysmacroname: grant_type=&grant_type;
/* initial validation checking */
%mp_abort(iftrue=(&grant_type ne authorization_code and &grant_type ne password

View File

@@ -39,22 +39,26 @@
@li mf_isblank.sas
@li mv_deletejes.sas
@param path= The full path (on SAS Drive) where the service will be created
@param name= The name of the service
@param desc= The description of the service
@param precode= Space separated list of filerefs, pointing to the code that
needs to be attached to the beginning of the service
@param code= Fileref(s) of the actual code to be added
@param access_token_var= The global macro variable to contain the access token
@param grant_type= valid values are "password" or "authorization_code" (unquoted).
The default is authorization_code.
@param replace= select NO to avoid replacing any existing service in that location
@param adapter= the macro uses the sasjs adapter by default. To use another
adapter, add a (different) fileref here.
@param contextname= Choose a specific context on which to run the Job. Leave
@param [in] path= The full path (on SAS Drive) where the service will be
created
@param [in] name= The name of the service
@param [in] desc= The description of the service
@param [in] precode= Space separated list of filerefs, pointing to the code
that needs to be attached to the beginning of the service
@param [in] code= Fileref(s) of the actual code to be added
@param [in] access_token_var= The global macro variable to contain the access
token
@param [in] grant_type= valid values are "password" or "authorization_code"
(unquoted). The default is authorization_code.
@param [in] replace=(YES) Select NO to avoid replacing any existing service in
that location
@param [in] adapter= the macro uses the sasjs adapter by default. To use
another adapter, add a (different) fileref here.
@param [in] contextname= Choose a specific context on which to run the Job. Leave
blank to use the default context. From Viya 3.5 it is possible to configure
a shared context - see
https://go.documentation.sas.com/?docsetId=calcontexts&docsetTarget=n1hjn8eobk5pyhn1wg3ja0drdl6h.htm&docsetVersion=3.5&locale=en
@param [in] mdebug=(0) set to 1 to enable DEBUG messages
@version VIYA V.03.04
@author Allan Bowe, source: https://github.com/sasjs/core
@@ -70,9 +74,17 @@ https://go.documentation.sas.com/?docsetId=calcontexts&docsetTarget=n1hjn8eobk5p
,grant_type=sas_services
,replace=YES
,adapter=sasjs
,debug=0
,mdebug=0
,contextname=
,debug=0 /* @TODO - Deprecate */
);
%local dbg;
%if &mdebug=1 %then %do;
%put &sysmacroname entry vars:;
%put _local_;
%end;
%else %let dbg=*;
%local oauth_bearer;
%if &grant_type=detect %then %do;
%if %symexist(&access_token_var) %then %let grant_type=authorization_code;
@@ -82,7 +94,6 @@ https://go.documentation.sas.com/?docsetId=calcontexts&docsetTarget=n1hjn8eobk5p
%let oauth_bearer=oauth_bearer=sas_services;
%let &access_token_var=;
%end;
%put &sysmacroname: grant_type=&grant_type;
/* initial validation checking */
%mp_abort(iftrue=(&grant_type ne authorization_code and &grant_type ne password
@@ -126,7 +137,7 @@ proc http method='GET' out=&fname1 &oauth_bearer
headers "Authorization"="Bearer &&&access_token_var";
%end;
run;
%if &debug %then %do;
%if &mdebug=1 %then %do;
data _null_;
infile &fname1;
input;
@@ -145,7 +156,8 @@ libname &libref1 JSON fileref=&fname1;
data _null_;
set &libref1..links;
if rel='members' then call symputx('membercheck',quote("&base_uri"!!trim(href)),'l');
if rel='members' then
call symputx('membercheck',quote("&base_uri"!!trim(href)),'l');
else if rel='self' then call symputx('parentFolderUri',href,'l');
run;
data _null_;
@@ -164,7 +176,7 @@ proc http method='GET'
%end;
'Accept'='application/vnd.sas.collection+json'
'Accept-Language'='string';
%if &debug=1 %then %do;
%if &mdebug=1 %then %do;
debug level = 3;
%end;
run;
@@ -219,16 +231,17 @@ run;
* These put statements are auto generated - to change the macro, change the
* source (mv_webout) and run `build.py`
*/
filename sasjs temp lrecl=3000;
filename &adapter temp lrecl=3000;
data _null_;
file sasjs;
file &adapter;
put "/* Created on %sysfunc(datetime(),datetime19.) by &sysuserid */";
/* WEBOUT BEGIN */
put ' ';
put '%macro mp_jsonout(action,ds,jref=_webout,dslabel=,fmt=Y,engine=PROCJSON,dbg=0 ';
put '%macro mp_jsonout(action,ds,jref=_webout,dslabel=,fmt=Y,engine=DATASTEP,dbg=0 ';
put ')/*/STORE SOURCE*/; ';
put '%put output location=&jref; ';
put '%if &action=OPEN %then %do; ';
put ' options nobomfile; ';
put ' data _null_;file &jref encoding=''utf-8''; ';
put ' put ''{"START_DTTM" : "'' "%sysfunc(datetime(),datetime20.3)" ''"''; ';
put ' run; ';
@@ -256,6 +269,64 @@ data _null_;
put ' %put &sysmacroname: &ds NOT FOUND!!!; ';
put ' %return; ';
put ' %end; ';
put ' %if &fmt=Y %then %do; ';
put ' %put converting every variable to a formatted variable; ';
put ' /* see mp_ds2fmtds.sas for source */ ';
put ' proc contents noprint data=&ds ';
put ' out=_data_(keep=name type length format formatl formatd varnum); ';
put ' run; ';
put ' proc sort; ';
put ' by varnum; ';
put ' run; ';
put ' %local fmtds; ';
put ' %let fmtds=%scan(&syslast,2,.); ';
put ' /* prepare formats and varnames */ ';
put ' data _null_; ';
put ' set &fmtds end=last; ';
put ' name=upcase(name); ';
put ' /* fix formats */ ';
put ' if type=2 or type=6 then do; ';
put ' length fmt $49.; ';
put ' if format='''' then fmt=cats(''$'',length,''.''); ';
put ' else if formatl=0 then fmt=cats(format,''.''); ';
put ' else fmt=cats(format,formatl,''.''); ';
put ' newlen=max(formatl,length); ';
put ' end; ';
put ' else do; ';
put ' if format='''' then fmt=''best.''; ';
put ' else if formatl=0 then fmt=cats(format,''.''); ';
put ' else if formatd=0 then fmt=cats(format,formatl,''.''); ';
put ' else fmt=cats(format,formatl,''.'',formatd); ';
put ' /* needs to be wide, for datetimes etc */ ';
put ' newlen=max(length,formatl,24); ';
put ' end; ';
put ' /* 32 char unique name */ ';
put ' newname=''sasjs''!!substr(cats(put(md5(name),$hex32.)),1,27); ';
put ' ';
put ' call symputx(cats(''name'',_n_),name,''l''); ';
put ' call symputx(cats(''newname'',_n_),newname,''l''); ';
put ' call symputx(cats(''len'',_n_),newlen,''l''); ';
put ' call symputx(cats(''fmt'',_n_),fmt,''l''); ';
put ' call symputx(cats(''type'',_n_),type,''l''); ';
put ' if last then call symputx(''nobs'',_n_,''l''); ';
put ' run; ';
put ' data &fmtds; ';
put ' /* rename on entry */ ';
put ' set &ds(rename=( ';
put ' %local i; ';
put ' %do i=1 %to &nobs; ';
put ' &&name&i=&&newname&i ';
put ' %end; ';
put ' )); ';
put ' %do i=1 %to &nobs; ';
put ' length &&name&i $&&len&i; ';
put ' &&name&i=left(put(&&newname&i,&&fmt&i)); ';
put ' drop &&newname&i; ';
put ' %end; ';
put ' if _error_ then call symputx(''syscc'',1012); ';
put ' run; ';
put ' %let ds=&fmtds; ';
put ' %end; /* &fmt=Y */ ';
put ' data _null_;file &jref mod ; ';
put ' put "["; call symputx(''cols'',0,''l''); ';
put ' proc sort ';
@@ -335,11 +406,11 @@ data _null_;
put '%end; ';
put ' ';
put '%else %if &action=CLOSE %then %do; ';
put ' data _null_;file &jref encoding=''utf-8''; ';
put ' data _null_;file &jref encoding=''utf-8'' mod; ';
put ' put "}"; ';
put ' run; ';
put '%end; ';
put '%mend; ';
put '%mend mp_jsonout; ';
put '%macro mv_webout(action,ds,fref=_mvwtemp,dslabel=,fmt=Y); ';
put '%global _webin_file_count _webin_fileuri _debug _omittextlog _webin_name ';
put ' sasjs_tables SYS_JES_JOB_URI; ';
@@ -467,7 +538,7 @@ data _null_;
put '%end; ';
put '%else %if &action=ARR or &action=OBJ %then %do; ';
put ' %mp_jsonout(&action,&ds,dslabel=&dslabel,fmt=&fmt ';
put ' ,jref=&fref,engine=PROCJSON,dbg=%str(&_debug) ';
put ' ,jref=&fref,engine=DATASTEP,dbg=%str(&_debug) ';
put ' ) ';
put '%end; ';
put '%else %if &action=CLOSE %then %do; ';
@@ -560,11 +631,12 @@ data _null_;
run;
/* insert the code, escaping double quotes and carriage returns */
%&dbg.put &sysmacroname: Creating final input file;
%local x fref freflist;
%let freflist= &adapter &precode &code ;
%do x=1 %to %sysfunc(countw(&freflist));
%let fref=%scan(&freflist,&x);
%put &sysmacroname: adding &fref;
%&dbg.put &sysmacroname: adding &fref fileref;
data _null_;
length filein 8 fileid 8;
filein = fopen("&fref","I",1,"B");
@@ -592,6 +664,14 @@ run;
rc =fput(fileid,'\');rc =fwrite(fileid);
rc =fput(fileid,'\');rc =fwrite(fileid);
end;
else if rec='01'x then do; /* Unprintable */
rc =fput(fileid,'\');rc =fwrite(fileid);
rc =fput(fileid,'u');rc =fwrite(fileid);
rc =fput(fileid,'0');rc =fwrite(fileid);
rc =fput(fileid,'0');rc =fwrite(fileid);
rc =fput(fileid,'0');rc =fwrite(fileid);
rc =fput(fileid,'1');rc =fwrite(fileid);
end;
else do;
rc =fput(fileid,rec);
rc =fwrite(fileid);
@@ -608,7 +688,12 @@ data _null_;
put '"}';
run;
/* now we can create the job!! */
%if &mdebug=1 and &SYS_PROCHTTP_STATUS_CODE ne 201 %then %do;
%put &sysmacroname: input about to be POSTed;
data _null_;infile &fname3;input;putlog _infile_;run;
%end;
%&dbg.put &sysmacroname: Creating the actual service!;
%local fname4;
%let fname4=%mf_getuniquefileref();
proc http method='POST'
@@ -621,22 +706,18 @@ proc http method='POST'
"Authorization"="Bearer &&&access_token_var"
%end;
"Accept"="application/vnd.sas.job.definition+json";
%if &debug=1 %then %do;
%if &mdebug=1 %then %do;
debug level = 3;
%end;
run;
/*data _null_;infile &fname4;input;putlog _infile_;run;*/
%if &mdebug=1 and &SYS_PROCHTTP_STATUS_CODE ne 201 %then %do;
%put &sysmacroname: output from POSTing job definition;
data _null_;infile &fname4;input;putlog _infile_;run;
%end;
%mp_abort(iftrue=(&SYS_PROCHTTP_STATUS_CODE ne 201)
,mac=&sysmacroname
,msg=%str(&SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE)
)
/* clear refs */
filename &fname1 clear;
filename &fname2 clear;
filename &fname3 clear;
filename &fname4 clear;
filename &adapter clear;
libname &libref1 clear;
/* get the url so we can give a helpful log message */
%local url;
@@ -651,6 +732,19 @@ data _null_;
call symputx('url',url);
run;
%if &mdebug=1 %then %do;
%put &sysmacroname exit vars:;
%put _local_;
%end;
%else %do;
/* clear refs */
filename &fname1 clear;
filename &fname2 clear;
filename &fname3 clear;
filename &fname4 clear;
filename &adapter clear;
libname &libref1 clear;
%end;
%put &sysmacroname: Job &name successfully created in &path;
%put &sysmacroname:;
@@ -660,4 +754,4 @@ run;
%put &sysmacroname:;
%put &sysmacroname:;
%mend;
%mend mv_createwebservice;

View File

@@ -44,7 +44,7 @@
%let oauth_bearer=oauth_bearer=sas_services;
%let &access_token_var=;
%end;
%put &sysmacroname: grant_type=&grant_type;
%mp_abort(iftrue=(&grant_type ne authorization_code and &grant_type ne password
and &grant_type ne sas_services
)

View File

@@ -39,7 +39,7 @@
%let oauth_bearer=oauth_bearer=sas_services;
%let &access_token_var=;
%end;
%put &sysmacroname: grant_type=&grant_type;
%mp_abort(iftrue=(&grant_type ne authorization_code and &grant_type ne password
and &grant_type ne sas_services
)

View File

@@ -54,7 +54,7 @@
%let oauth_bearer=oauth_bearer=sas_services;
%let &access_token_var=;
%end;
%put &sysmacroname: grant_type=&grant_type;
%mp_abort(iftrue=(&grant_type ne authorization_code and &grant_type ne password
and &grant_type ne sas_services
)

View File

@@ -10,17 +10,20 @@
,outfile=/tmp/some_job.sas
)
@param [in] access_token_var= The global macro variable to contain the access token
@param [in] access_token_var= The global macro variable to contain the access
token
@param [in] grant_type= valid values:
* password
* authorization_code
* detect - will check if access_token exists, if not will use sas_services if
a SASStudioV session else authorization_code. Default option.
* sas_services - will use oauth_bearer=sas_services
@li password
@liauthorization_code
@li detect - will check if access_token exists, if not will use sas_services
if a SASStudioV session else authorization_code. Default option.
@li sas_services - will use oauth_bearer=sas_services
@param [in] path= The SAS Drive path of the job
@param [in] name= The name of the job
@param [out] outref= A fileref to which to write the source code
@param [out] outfile= A file to which to write the source code
@param [in] mdebug=(0) set to 1 to enable DEBUG messages
@param [out] outref=(0) A fileref to which to write the source code (will be
created with a TEMP engine)
@param [out] outfile=(0) A file to which to write the source code
@version VIYA V.03.04
@author Allan Bowe, source: https://github.com/sasjs/core
@@ -39,7 +42,15 @@
,contextName=SAS Job Execution compute context
,access_token_var=ACCESS_TOKEN
,grant_type=sas_services
,mdebug=0
);
%local dbg;
%if &mdebug=1 %then %do;
%put &sysmacroname entry vars:;
%put _local_;
%end;
%else %let dbg=*;
%local oauth_bearer;
%if &grant_type=detect %then %do;
%if %symexist(&access_token_var) %then %let grant_type=authorization_code;
@@ -133,18 +144,30 @@ data _null_;
run;
%inc "&fpath3..lua";
/* export to desired destination */
data _null_;
%if &outref=0 %then %do;
%if "&outref"="0" %then %do;
data _null_;
file "&outfile" lrecl=32767;
%end;
%else %do;
%end;
%else %do;
filename &outref temp;
data _null_;
file &outref;
%end;
%end;
infile &fname2;
input;
put _infile_;
&dbg. putlog _infile_;
run;
filename &fname1 clear;
filename &fname2 clear;
filename &fname3 clear;
%mend;
%if &mdebug=1 %then %do;
%put &sysmacroname exit vars:;
%put _local_;
%end;
%else %do;
/* clear refs */
filename &fname1 clear;
filename &fname2 clear;
filename &fname3 clear;
%end;
%mend mv_getjobcode;

View File

@@ -1,21 +1,19 @@
/**
@file
@brief Extract the log from a completed SAS Viya Job
@details Extracts log from a Viya job and writes it out to a fileref
@details Extracts log from a Viya job and writes it out to a fileref.
To query the job, you need the URI. Sample code for achieving this
is provided below.
## Example
First, compile the macros:
%* First, compile the macros;
filename mc url
"https://raw.githubusercontent.com/sasjs/core/main/all.sas";
%inc mc;
Next, create a job (in this case, a web service):
%* Next, create a job (in this case, a web service);
filename ft15f001 temp;
parmcards4;
data ;
@@ -31,39 +29,52 @@
;;;;
%mv_createwebservice(path=/Public/temp,name=demo)
Execute it:
%* Execute it;
%mv_jobexecute(path=/Public/temp
,name=demo
,outds=work.info
)
Wait for it to finish, and grab the uri:
data _null_;
%* Wait for it to finish;
data work.info;
set work.info;
if method='GET' and rel='self';
where method='GET' and rel='state';
run;
%mv_jobwaitfor(ALL,inds=work.info,outds=work.jobstates)
%* and grab the uri;
data _null_;
set work.jobstates;
call symputx('uri',uri);
run;
Finally, fetch the log:
%* Finally, fetch the log;
%mv_getjoblog(uri=&uri,outref=mylog)
This macro is used by the mv_jobwaitfor.sas macro, which is generally a more
convenient way to wait for the job to finish before fetching the log.
If the remote session calls `endsas` then it is not possible to get the log
from the provided uri, and so the log from the parent session is fetched
instead. This happens for a 400 response, eg below:
@param [in] access_token_var= The global macro variable to contain the access token
ErrorResponse[version=2,status=400,err=5113,id=,message=The session
requested is currently in a failed or stopped state.,detail=[path:
/compute/sessions/LONGURI-ses0006/jobs/LONGURI/log/content, traceId: 63
51aa617d01fd2b],remediation=Correct the errors in the session request,
and create a new session.,targetUri=<null>,errors=[],links=[]]
@param [in] access_token_var= The global macro variable to contain the access
token
@param [in] mdebug= set to 1 to enable DEBUG messages
@param [in] grant_type= valid values:
@li password
@li authorization_code
@li detect - will check if access_token exists, if not will use sas_services if
a SASStudioV session else authorization_code. Default option.
@li detect - will check if access_token exists, if not will use sas_services
if a SASStudioV session else authorization_code. Default option.
@li sas_services - will use oauth_bearer=sas_services.
@param [in] uri= The uri of the running job for which to fetch the status,
in the format `/jobExecution/jobs/$UUID/state` (unquoted).
in the format `/jobExecution/jobs/$UUID` (unquoted).
@param [out] outref= The output fileref to which to APPEND the log (is always
appended).
@@ -85,6 +96,13 @@
,grant_type=sas_services
,mdebug=0
);
%local dbg;
%if &mdebug=1 %then %do;
%put &sysmacroname entry vars:;
%put _local_;
%end;
%else %let dbg=*;
%local oauth_bearer;
%if &grant_type=detect %then %do;
%if %symexist(&access_token_var) %then %let grant_type=authorization_code;
@@ -114,7 +132,7 @@ data _null_;
call symputx('errflg',1);
call symputx('errmsg',
"URI should be in format /jobExecution/jobs/$$$$UUID$$$$"
!!" but is actually like: &uri",'l');
!!" but is actually like:"!!uri,'l');
end;
run;
@@ -147,6 +165,10 @@ proc http method='GET' out=&fname1 &oauth_bearer
%end;
;
run;
%if &mdebug=1 %then %do;
%put &sysmacroname: fetching log loc from &uri;
data _null_;infile &fname1;input;putlog _infile_;run;
%end;
%if &SYS_PROCHTTP_STATUS_CODE ne 200 and &SYS_PROCHTTP_STATUS_CODE ne 201 %then
%do;
data _null_;infile &fname1;input;putlog _infile_;run;
@@ -184,43 +206,74 @@ run;
data _null_;
infile &fname2;
input;
uri=_infile_;
uri=cats(_infile_);
if length(uri)<12 then do;
call symputx('errflg',1);
call symputx('errmsg',"URI is invalid (too short) - '&uri'",'l');
end;
if scan(uri,1) ne 'files' or scan(uri,2) ne 'files' then do;
else if (scan(uri,1,'/') ne 'compute' or scan(uri,2,'/') ne 'sessions')
and (scan(uri,1,'/') ne 'files' or scan(uri,2,'/') ne 'files')
then do;
call symputx('errflg',1);
call symputx('errmsg',
"URI should be in format /files/files/$$$$UUID$$$$"
!!" but is actually like: &uri",'l');
"URI should be in format /compute/sessions/$$$$UUID$$$$/jobs/$$$$UUID$$$$"
!!" or /files/files/$$$$UUID$$$$"
!!" but is actually like:"!!uri,'l');
end;
else do;
call symputx('errflg',0,'l');
call symputx('logloc',uri,'l');
end;
call symputx('errflg',0,'l');
call symputx('logloc',uri,'l');
run;
%mp_abort(iftrue=(&errflg=1)
%mp_abort(iftrue=(%str(&errflg)=1)
,mac=&sysmacroname
,msg=%str(&errmsg)
)
/* we have a log uri - now fetch the log */
%&dbg.put &sysmacroname: querying &base_uri&logloc/content;
proc http method='GET' out=&fname1 &oauth_bearer
url="&base_uri&logloc/content";
url="&base_uri&logloc/content?limit=10000";
headers
%if &grant_type=authorization_code %then %do;
"Authorization"="Bearer &&&access_token_var"
%end;
;
run;
%if &SYS_PROCHTTP_STATUS_CODE ne 200 and &SYS_PROCHTTP_STATUS_CODE ne 201 %then
%do;
%if &mdebug=1 %then %do;
%put &sysmacroname: fetching log content from &base_uri&logloc/content;
data _null_;infile &fname1;input;putlog _infile_;run;
%end;
%if &SYS_PROCHTTP_STATUS_CODE=400 %then %do;
/* fetch log from parent session */
%let logloc=%substr(&logloc,1,%index(&logloc,%str(/jobs/))-1);
%&dbg.put &sysmacroname: Now querying &base_uri&logloc/log/content;
proc http method='GET' out=&fname1 &oauth_bearer
url="&base_uri&logloc/log/content?limit=10000";
headers
%if &grant_type=authorization_code %then %do;
"Authorization"="Bearer &&&access_token_var"
%end;
;
run;
%if &mdebug=1 %then %do;
%put &sysmacroname: fetching log content from &base_uri&logloc/log/content;
data _null_;infile &fname1;input;putlog _infile_;run;
%end;
%end;
%if &SYS_PROCHTTP_STATUS_CODE ne 200 and &SYS_PROCHTTP_STATUS_CODE ne 201
%then %do;
%if &mdebug ne 1 %then %do; /* have already output above */
data _null_;infile &fname1;input;putlog _infile_;run;
%end;
%mp_abort(mac=&sysmacroname
,msg=%str(logfetch: &SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE)
)
%end;
data _null_;
file "&fpath3..lua";
put '
@@ -260,9 +313,10 @@ run;
filename &fname3 clear;
%end;
%else %do;
%put &sysmacroname exit vars:;
%put _local_;
%end;
%mend;
%mend mv_getjoblog;

232
viya/mv_getjobresult.sas Normal file
View File

@@ -0,0 +1,232 @@
/**
@file
@brief Extract the result from a completed SAS Viya Job
@details Extracts result from a Viya job and writes it out to a fileref
and/or a JSON-engine library.
To query the job, you need the URI. Sample code for achieving this
is provided below.
## Example
First, compile the macros:
filename mc url
"https://raw.githubusercontent.com/sasjs/core/main/all.sas";
%inc mc;
Next, create a job (in this case, a web service):
filename ft15f001 temp;
parmcards4;
data test;
rand=ranuni(0)*1000;
do x=1 to rand;
y=rand*4;
output;
end;
run;
proc sort data=&syslast
by descending y;
run;
%webout(OPEN)
%webout(OBJ, test)
%webout(CLOSE)
;;;;
%mv_createwebservice(path=/Public/temp,name=demo)
Execute it:
%mv_jobexecute(path=/Public/temp
,name=demo
,outds=work.info
)
Wait for it to finish, and grab the uri:
data _null_;
set work.info;
if method='GET' and rel='self';
call symputx('uri',uri);
run;
Finally, fetch the result (In this case, WEBOUT):
%mv_getjobresult(uri=&uri,result=WEBOUT_JSON,outref=myweb,outlib=myweblib)
@param [in] access_token_var= The global macro variable containing the access
token
@param [in] mdebug= set to 1 to enable DEBUG messages
@param [in] grant_type= valid values:
@li password
@li authorization_code
@li detect - will check if access_token exists, if not will use sas_services
if a SASStudioV session else authorization_code. Default option.
@li sas_services - will use oauth_bearer=sas_services.
@param [in] uri= The uri of the running job for which to fetch the status,
in the format `/jobExecution/jobs/$UUID` (unquoted).
@param [out] result= (WEBOUT_JSON) The result type to capture. Resolves
to "_[column name]" from the results table when parsed with the JSON libname
engine. Example values:
@li WEBOUT_JSON
@li WEBOUT_TXT
@param [out] outref= (0) The output fileref to which to write the results
@param [out] outlib= (0) The output library to which to assign the results
(assumes the data is in JSON format)
@version VIYA V.03.05
@author Allan Bowe, source: https://github.com/sasjs/core
<h4> SAS Macros </h4>
@li mp_abort.sas
@li mp_binarycopy.sas
@li mf_getplatform.sas
@li mf_existfileref.sas
**/
%macro mv_getjobresult(uri=0
,contextName=SAS Job Execution compute context
,access_token_var=ACCESS_TOKEN
,grant_type=sas_services
,mdebug=0
,result=WEBOUT_JSON
,outref=0
,outlib=0
);
%local dbg;
%if &mdebug=1 %then %do;
%put &sysmacroname entry vars:;
%put _local_;
%end;
%else %let dbg=*;
%local oauth_bearer;
%if &grant_type=detect %then %do;
%if %symexist(&access_token_var) %then %let grant_type=authorization_code;
%else %let grant_type=sas_services;
%end;
%if &grant_type=sas_services %then %do;
%let oauth_bearer=oauth_bearer=sas_services;
%let &access_token_var=;
%end;
%mp_abort(iftrue=(&grant_type ne authorization_code and &grant_type ne password
and &grant_type ne sas_services
)
,mac=&sysmacroname
,msg=%str(Invalid value for grant_type: &grant_type)
)
/* validation in datastep for better character safety */
%local errmsg errflg;
data _null_;
uri=symget('uri');
if length(uri)<12 then do;
call symputx('errflg',1);
call symputx('errmsg',"URI is invalid (too short) - '&uri'",'l');
end;
if scan(uri,-1)='state' or scan(uri,1) ne 'jobExecution' then do;
call symputx('errflg',1);
call symputx('errmsg',
"URI should be in format /jobExecution/jobs/$$$$UUID$$$$"
!!" but is actually like: &uri",'l');
end;
run;
%mp_abort(iftrue=(&errflg=1)
,mac=&sysmacroname
,msg=%str(&errmsg)
)
%if &outref ne 0 and %mf_existfileref(&outref) ne 1 %then %do;
filename &outref temp;
%end;
options noquotelenmax;
%local base_uri; /* location of rest apis */
%let base_uri=%mf_getplatform(VIYARESTAPI);
/* fetch job info */
%local fname1;
%let fname1=%mf_getuniquefileref();
proc http method='GET' out=&fname1 &oauth_bearer
url="&base_uri&uri";
headers "Accept"="application/json"
%if &grant_type=authorization_code %then %do;
"Authorization"="Bearer &&&access_token_var"
%end;
;
run;
%if &SYS_PROCHTTP_STATUS_CODE ne 200 and &SYS_PROCHTTP_STATUS_CODE ne 201 %then
%do;
data _null_;infile &fname1;input;putlog _infile_;run;
%mp_abort(mac=&sysmacroname
,msg=%str(&SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE)
)
%end;
%if &mdebug=1 %then %do;
data _null_;
infile &fname1 lrecl=32767;
input;
putlog _infile_;
run;
%end;
/* extract results link */
%local lib1 resuri;
%let lib1=%mf_getuniquelibref();
libname &lib1 JSON fileref=&fname1;
data _null_;
set &lib1..results;
call symputx('resuri',_&result,'l');
&dbg putlog "&sysmacroname results: " (_all_)(=);
run;
%mp_abort(iftrue=("&resuri"=".")
,mac=&sysmacroname
,msg=%str(Variable _&result did not exist in the response json)
)
/* extract results */
%local fname2;
%let fname2=%mf_getuniquefileref();
proc http method='GET' out=&fname2 &oauth_bearer
url="&base_uri&resuri/content?limit=10000";
headers "Accept"="application/json"
%if &grant_type=authorization_code %then %do;
"Authorization"="Bearer &&&access_token_var"
%end;
;
run;
%if &mdebug=1 %then %do;
data _null_;
infile &fname2 lrecl=32767;
input;
putlog _infile_;
run;
%end;
%if &outref ne 0 %then %do;
filename &outref temp;
%mp_binarycopy(inref=&fname2,outref=&outref)
%end;
%if &outlib ne 0 %then %do;
libname &outlib JSON fileref=&fname2;
%end;
%if &mdebug=0 %then %do;
filename &fname1 clear;
filename &fname2 clear;
libname &lib1 clear;
%end;
%else %do;
%put &sysmacroname exit vars:;
%put _local_;
%end;
%mend mv_getjobresult;

View File

@@ -23,24 +23,25 @@
,paramstring=%str("macvarname":"macvarvalue","answer":42)
)
@param [in] access_token_var= The global macro variable to contain the access token
@param [in] access_token_var= The global macro variable to contain the access
token
@param [in] grant_type= valid values:
* password
* authorization_code
* detect - will check if access_token exists, if not will use sas_services if
a SASStudioV session else authorization_code. Default option.
* sas_services - will use oauth_bearer=sas_services
@li password
@li authorization_code
@li detect - will check if access_token exists, if not will use sas_services
if a SASStudioV session else authorization_code. Default option.
@li sas_services - will use oauth_bearer=sas_services
@param [in] path= The SAS Drive path to the job being executed
@param [in] name= The name of the job to execute
@param [in] paramstring= A JSON fragment with name:value pairs, eg: `"name":"value"`
or "name":"value","name2":42`. This will need to be wrapped in `%str()`.
@param [in] paramstring= A JSON fragment with name:value pairs, eg:
`"name":"value"` or "name":"value","name2":42`. This will need to be
wrapped in `%str()`.
@param [in] contextName= Context name with which to run the job.
Default = `SAS Job Execution compute context`
@param [out] outds= The output dataset containing links (Default=work.mv_jobexecute)
@param [in] mdebug= set to 1 to enable DEBUG messages
@param [out] outds= (work.mv_jobexecute) The output dataset containing links
@version VIYA V.03.04
@@ -62,7 +63,15 @@
,grant_type=sas_services
,paramstring=0
,outds=work.mv_jobexecute
,mdebug=0
);
%local dbg;
%if &mdebug=1 %then %do;
%put &sysmacroname entry vars:;
%put _local_;
%end;
%else %let dbg=*;
%local oauth_bearer;
%if &grant_type=detect %then %do;
%if %symexist(&access_token_var) %then %let grant_type=authorization_code;
@@ -164,9 +173,14 @@ data &outds;
_program="&path/&name";
run;
/* clear refs */
filename &fname0 clear;
filename &fname1 clear;
libname &libref;
%mend;
%if &mdebug=1 %then %do;
%put &sysmacroname exit vars:;
%put _local_;
%end;
%else %do;
/* clear refs */
filename &fname0 clear;
filename &fname1 clear;
libname &libref;
%end;
%mend mv_jobexecute;

View File

@@ -97,8 +97,8 @@
run;
@param [in] access_token_var= The global macro variable to contain the access
token
@param [in] access_token_var= The global macro variable to contain the
access token
@param [in] grant_type= valid values:
@li password
@li authorization_code
@@ -136,6 +136,13 @@
,raise_err=0
,mdebug=0
);
%local dbg;
%if &mdebug=1 %then %do;
%put &sysmacroname entry vars:;
%put _local_;
%end;
%else %let dbg=*;
%local oauth_bearer;
%if &grant_type=detect %then %do;
%if %symexist(&access_token_var) %then %let grant_type=authorization_code;
@@ -230,123 +237,150 @@ data;run;%let jdswaitfor=&syslast;
/* start loop */
%do fid=1 %to &flowcnt;
%put preparing job attributes for flow &&flow&fid;
%local jds jcnt;
data &jds(drop=_contextName _program);
set &inds(where=(flow_id=&&flow&fid));
if _contextName='' then _contextName="SAS Job Execution compute context";
call symputx(cats('job',_n_),_program,'l');
call symputx(cats('context',_n_),_contextName,'l');
call symputx('jcnt',_n_,'l');
run;
%put exporting job variables in json format;
%do jid=1 %to &jcnt;
data &jjson;
set &jds;
if _n_=&jid then do;
output;
stop;
end;
run;
proc json out=&jfref;
export &jjson / nosastags fmtnumeric;
run;
data _null_;
infile &jfref lrecl=32767;
input;
jparams='jparams'!!left(symget('jid'));
call symputx(jparams,substr(_infile_,3,length(_infile_)-4));
run;
%local jobuid&jid;
%let jobuid&jid=0; /* used in next loop */
%end;
%local concurrency completed;
%let concurrency=0;
%let completed=0;
proc sql; drop table &jdsrunning;
%do jid=1 %to &jcnt;
/**
* now we can execute the jobs up to the maxconcurrency setting
*/
%if "&&job&jid" ne "0" %then %do; /* this var is zero if job finished */
/* check to see if the job finished in the previous round */
%if %sysfunc(exist(&outds))=1 %then %do;
%local jobcheck; %let jobcheck=0;
proc sql noprint;
select count(*) into: jobcheck
from &outds where uuid="&&jobuid&jid";
%if &jobcheck>0 %then %do;
%put &&job&jid in flow &fid with uid &&jobuid&jid completed!;
%let job&jid=0;
%if not ( &raise_err and &syscc ) %then %do;
%put preparing job attributes for flow &&flow&fid;
%local jds jcnt;
data &jds(drop=_contextName _program);
set &inds(where=(flow_id=&&flow&fid));
if _contextName='' then _contextName="SAS Job Execution compute context";
call symputx(cats('job',_n_),_program,'l');
call symputx(cats('context',_n_),_contextName,'l');
call symputx('jcnt',_n_,'l');
run;
%put exporting job variables in json format;
%do jid=1 %to &jcnt;
data &jjson;
set &jds;
if _n_=&jid then do;
output;
stop;
end;
run;
proc json out=&jfref;
export &jjson / nosastags fmtnumeric;
run;
data _null_;
infile &jfref lrecl=32767;
input;
jparams='jparams'!!left(symget('jid'));
call symputx(jparams,substr(_infile_,3,length(_infile_)-4));
run;
%local jobuid&jid;
%let jobuid&jid=0; /* used in next loop */
%end;
%local concurrency completed;
%let concurrency=0;
%let completed=0;
proc sql; drop table &jdsrunning;
%do jid=1 %to &jcnt;
/**
* now we can execute the jobs up to the maxconcurrency setting
*/
%if "&&job&jid" ne "0" %then %do; /* this var is zero if job finished */
/* check to see if the job finished in the previous round */
%if %sysfunc(exist(&outds))=1 %then %do;
%local jobcheck; %let jobcheck=0;
proc sql noprint;
select count(*) into: jobcheck
from &outds where uuid="&&jobuid&jid";
%if &jobcheck>0 %then %do;
%put &&job&jid in flow &fid with uid &&jobuid&jid completed!;
%let job&jid=0;
%end;
%end;
/* check if job was triggered and, if
so, if we have enough slots to run? */
%if ("&&jobuid&jid"="0") and (&concurrency<&maxconcurrency) %then %do;
/* But only start if no issues detected so far */
%if not ( &raise_err and &syscc ) %then %do;
%local jobname jobpath;
%let jobname=%scan(&&job&jid,-1,/);
%let jobpath=
%substr(&&job&jid,1,%length(&&job&jid)-%length(&jobname)-1);
%put executing &jobpath/&jobname with paramstring &&jparams&jid;
%mv_jobexecute(path=&jobpath
,name=&jobname
,paramstring=%superq(jparams&jid)
,outds=&jdsapp
)
data &jdsapp;
format jobparams $32767.;
set &jdsapp(where=(method='GET' and rel='state'));
jobparams=symget("jparams&jid");
/* uri here has the /state suffix */
uuid=scan(uri,-2,'/');
call symputx("jobuid&jid",uuid,'l');
run;
proc append base=&jdsrunning data=&jdsapp;
run;
%let concurrency=%eval(&concurrency+1);
/* sleep one second after every request to smooth the impact */
data _null_;
call sleep(1,1);
run;
%end;
%else %do; /* Job was skipped due to problems */
%put jobid &&job&jid in flow &fid skipped due to SYSCC (&syscc);
%let completed = %eval(&completed+1);
%let job&jid=0; /* Indicate job has finished */
%end;
%end;
%end;
%if &jid=&jcnt %then %do;
/* we are at the end of the loop - check which jobs have finished */
%mv_jobwaitfor(ANY,inds=&jdsrunning,outds=&jdswaitfor,outref=&outref
,raise_err=&raise_err,mdebug=&mdebug)
%local done;
%let done=%mf_nobs(&jdswaitfor);
%if &done>0 %then %do;
%let completed=%eval(&completed+&done);
%let concurrency=%eval(&concurrency-&done);
data &jdsapp;
set &jdswaitfor;
flow_id=&&flow&fid;
uuid=scan(uri,-1,'/');
run;
proc append base=&outds data=&jdsapp;
run;
%end;
proc sql;
delete from &jdsrunning
where uuid in (select uuid from &outds
where state in ('canceled','completed','failed')
);
/* check if job was triggered and if so, if we have enough slots to run */
%if "&&jobuid&jid"="0" and &concurrency<&maxconcurrency %then %do;
%local jobname jobpath;
%let jobname=%scan(&&job&jid,-1,/);
%let jobpath=
%substr(&&job&jid,1,%length(&&job&jid)-%length(&jobname)-1);
%put executing &jobpath/&jobname with paramstring &&jparams&jid;
%mv_jobexecute(path=&jobpath
,name=&jobname
,paramstring=%superq(jparams&jid)
,outds=&jdsapp
)
data &jdsapp;
format jobparams $32767.;
set &jdsapp(where=(method='GET' and rel='state'));
jobparams=symget("jparams&jid");
/* uri here has the /state suffix */
uuid=scan(uri,-2,'/');
call symputx("jobuid&jid",uuid,'l');
run;
proc append base=&jdsrunning data=&jdsapp;
run;
%let concurrency=%eval(&concurrency+1);
/* sleep one second after every request to smooth the impact */
data _null_;
call sleep(1,1);
run;
/* loop again if jobs are left */
%if &completed < &jcnt %then %do;
%let jid=0;
%put looping flow &fid again;
%put &completed of &jcnt jobs completed, &concurrency jobs running;
%end;
%end;
%end;
%if &jid=&jcnt %then %do;
/* we are at the end of the loop - time to see which jobs have finished */
%mv_jobwaitfor(ANY,inds=&jdsrunning,outds=&jdswaitfor,outref=&outref
,raise_err=&raise_err)
%local done;
%let done=%mf_nobs(&jdswaitfor);
%if &done>0 %then %do;
%let completed=%eval(&completed+&done);
%let concurrency=%eval(&concurrency-&done);
data &jdsapp;
set &jdswaitfor;
flow_id=&&flow&fid;
uuid=scan(uri,-1,'/');
run;
proc append base=&outds data=&jdsapp;
run;
%end;
proc sql;
delete from &jdsrunning
where uuid in (select uuid from &outds
where state in ('canceled','completed','failed')
);
/* loop again if jobs are left */
%if &completed < &jcnt %then %do;
%let jid=0;
%put looping flow &fid again - &completed of &jcnt jobs completed,
&concurrency jobs running;
%end;
%end;
%end;
%else %do;
%put Flow &&flow&fid skipped due to SYSCC (&syscc);
%end;
/* back up and execute the next flow */
%end;
%if &mdebug=1 %then %do;
%put &sysmacroname exit vars:;
%put _local_;
%end;
%mend;
%mend mv_jobflow;

View File

@@ -1,6 +1,6 @@
/**
@file
@brief Takes a dataset of running jobs and waits for ANY or ALL of them to complete
@brief Takes a table of running jobs and waits for ANY/ALL of them to complete
@details Will poll `/jobs/{jobId}/state` at set intervals until ANY or ALL
jobs are completed. Completion is determined by reference to the returned
_state_, as per the following table:
@@ -55,13 +55,14 @@
%mv_deletejes(path=/Public/temp,name=demo)
@param [in] access_token_var= The global macro variable to contain the access token
@param [in] access_token_var= The global macro variable to contain the access
token
@param [in] grant_type= valid values:
- password
- authorization_code
- detect - will check if access_token exists, if not will use sas_services if
a SASStudioV session else authorization_code. Default option.
- detect - will check if access_token exists, if not will use sas_services
if a SASStudioV session else authorization_code. Default option.
- sas_services - will use oauth_bearer=sas_services
@param [in] action=Either ALL (to wait for every job) or ANY (if one job
@@ -72,9 +73,11 @@
should be in a `_program` variable.
@param [in] raise_err=0 Set to 1 to raise SYSCC when a job does not complete
succcessfully
@param [in] mdebug= set to 1 to enable DEBUG messages
@param [out] outds= The output dataset containing the list of states by job
(default=work.mv_jobexecute)
@param [out] outref= A fileref to which the spawned job logs should be appended.
@param [out] outref= A fileref to which the spawned job logs should be
appended.
@version VIYA V.03.04
@author Allan Bowe, source: https://github.com/sasjs/core
@@ -97,7 +100,15 @@
,outds=work.mv_jobwaitfor
,outref=0
,raise_err=0
,mdebug=0
);
%local dbg;
%if &mdebug=1 %then %do;
%put &sysmacroname entry vars:;
%put _local_;
%end;
%else %let dbg=*;
%local oauth_bearer;
%if &grant_type=detect %then %do;
%if %symexist(&access_token_var) %then %let grant_type=authorization_code;
@@ -155,7 +166,8 @@ run;
%let fname0=%mf_getuniquefileref();
data &outds;
format _program uri $128. state $32. stateDetails $32. timestamp datetime19. jobparams $32767.;
format _program uri $128. state $32. stateDetails $32. timestamp datetime19.
jobparams $32767.;
stop;
run;
@@ -168,8 +180,8 @@ run;
"Authorization"="Bearer &&&access_token_var"
%end; ;
run;
%if &SYS_PROCHTTP_STATUS_CODE ne 200 and &SYS_PROCHTTP_STATUS_CODE ne 201 %then
%do;
%if &SYS_PROCHTTP_STATUS_CODE ne 200 and &SYS_PROCHTTP_STATUS_CODE ne 201
%then %do;
data _null_;infile &fname0;input;putlog _infile_;run;
%mp_abort(mac=&sysmacroname
,msg=%str(&SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE)
@@ -205,7 +217,7 @@ run;
%let joburi&i=0; /* do not re-check */
/* fetch log */
%if %str(&outref) ne 0 %then %do;
%mv_getjoblog(uri=&plainuri,outref=&outref)
%mv_getjoblog(uri=&plainuri,outref=&outref,mdebug=&mdebug)
%end;
%end;
%else %if &status=idle or &status=pending or &status=running %then %do;
@@ -220,10 +232,11 @@ run;
%end;
%if (&raise_err) %then %do;
%if (&status = canceled or &status = failed or %length(&stateDetails)>0) %then %do;
%if (&status = canceled or &status = failed or %length(&stateDetails)>0)
%then %do;
%if ("&stateDetails" = "%str(war)ning") %then %let SYSCC=4;
%else %let SYSCC=5;
%put %str(ERR)OR: Job &&jobname&i. did not complete successfully. &stateDetails;
%put %str(ERR)OR: Job &&jobname&i. did not complete. &stateDetails;
%return;
%end;
%end;
@@ -238,7 +251,12 @@ run;
%end;
%end;
/* clear refs */
filename &fname0 clear;
%mend;
%if &mdebug=1 %then %do;
%put &sysmacroname exit vars:;
%put _local_;
%end;
%else %do;
/* clear refs */
filename &fname0 clear;
%end;
%mend mv_jobwaitfor;

View File

@@ -1,14 +1,22 @@
/**
@file mv_registerclient.sas
@brief Register Client and Secret (admin task)
@details When building apps on SAS Viya, an client id and secret is required.
This macro will obtain the Consul Token and use that to call the Web Service.
@details When building apps on SAS Viya, an client id and secret are sometimes
required. In order to generate them, filesystem access to the Consul Token
is needed (it is not enough to be in the SASAdministrator group in SAS
Environment Manager).
more info: https://developer.sas.com/reference/auth/#register
and:
http://proc-x.com/2019/01/authentication-to-sas-viya-a-couple-of-approaches
If you are registering a lot of clients / secrets, you may find it more
convenient to use the [Viya Token Generator]
(https://sasjs.io/apps/#viya-client-token-generator) (a SASjs Web App to
automate the generation of clients & secrets with various settings).
The default viyaroot location is /opt/sas/viya/config
For further information on clients / secrets, see;
@li https://developer.sas.com/reference/auth/#register
@li https://proc-x.com/2019/01/authentication-to-sas-viya-a-couple-of-approaches
@li https://cli.sasjs.io/faq/#how-can-i-obtain-a-viya-client-and-secret
The default viyaroot location is: `/opt/sas/viya/config`
Usage:
@@ -33,16 +41,19 @@
)
@param client_id= The client name. Auto generated if blank.
@param client_secret= Client secret Auto generated if client is blank.
@param scopes= list of space-seperated unquoted scopes (default is openid)
@param grant_type= valid values are "password" or "authorization_code"
(unquoted)
@param outds= the dataset to contain the registered client id and secret
@param access_token_validity= The duration of validity of the access token
in seconds. A value of DEFAULT will omit the entry (and use system default)
@param refresh_token_validity= The duration of validity of the refresh token
in seconds. A value of DEFAULT will omit the entry (and use system default)
@param name= A human readable name for the client
@param client_secret= Client secret. Auto generated if client is blank.
@param scopes=(openid) List of space-seperated unquoted scopes
@param grant_type=(authorization_code|refresh_token) Valid values are
"password" or "authorization_code" (unquoted)
@param outds=(mv_registerclient) The dataset to contain the registered client
id and secret
@param access_token_validity=(DEFAULT) The duration of validity of the access
token in seconds. A value of DEFAULT will omit the entry (and use system
default)
@param refresh_token_validity=(DEFAULT) The duration of validity of the
refresh token in seconds. A value of DEFAULT will omit the entry (and use
system default)
@param name= An optional, human readable name for the client
@param required_user_groups= A list of group names. If a user does not belong
to all the required groups, the user will not be authenticated and no tokens
are issued to this client for that user. If this field is not specified,
@@ -51,8 +62,8 @@
apply. Setting this to true will autoapprove all the client scopes.
@param use_session= If true, access tokens issued to this client will be
associated with an HTTP session and revoked upon logout or time-out.
@param outjson= A dataset containing the lines of JSON submitted. Useful
for debugging. Default= _null_.
@param outjson= (_null_) A dataset containing the lines of JSON submitted.
Useful for debugging.
@version VIYA V.03.04
@author Allan Bowe, source: https://github.com/sasjs/core
@@ -89,12 +100,26 @@
options noquotelenmax;
/* first, get consul token needed to get client id / secret */
%let tokloc=/etc/SASSecurityCertificateFramework/tokens/consul/default;
%let tokloc=%mf_loc(VIYACONFIG)&tokloc/client.token;
%mp_abort(iftrue=(%sysfunc(fileexist(&tokloc))=0)
,mac=&sysmacroname
,msg=%str(Unable to access the consul token at &tokloc)
)
%let consul_token=0;
data _null_;
infile "%mf_loc(VIYACONFIG)&tokloc/client.token";
infile "&tokloc";
input token:$64.;
call symputx('consul_token',token);
run;
%mp_abort(iftrue=("&consul_token"="0")
,mac=&sysmacroname
,msg=%str(Unable to source the consul token from &tokloc)
)
%local base_uri; /* location of rest apis */
%let base_uri=%mf_getplatform(VIYARESTAPI);
@@ -241,4 +266,4 @@ filename &fname2 clear;
filename &fname3 clear;
libname &libref clear;
%mend;
%mend mv_registerclient;