Macros for SAS Application Developers
https://github.com/sasjs/core
Loading...
Searching...
No Matches
mv_getjobresult.sas
Go to the documentation of this file.
1/**
2 @file
3 @brief Extract the result from a completed SAS Viya Job
4 @details Extracts result from a Viya job and writes it out to a fileref
5 and/or a JSON-engine library.
6
7 To query the job, you need the URI. Sample code for achieving this
8 is provided below.
9
10 ## Example
11
12 First, compile the macros:
13
14 filename mc url
15 "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
16 %inc mc;
17
18 Next, create a job (in this case, a web service):
19
20 filename ft15f001 temp;
21 parmcards4;
22 data test;
23 rand=ranuni(0)*1000;
24 do x=1 to rand;
25 y=rand*4;
26 output;
27 end;
28 run;
29 proc sort data=&syslast
30 by descending y;
31 run;
32 %webout(OPEN)
33 %webout(OBJ, test)
34 %webout(CLOSE)
35 ;;;;
36 %mv_createwebservice(path=/Public/temp,name=demo)
37
38 Execute it:
39
40 %mv_jobexecute(path=/Public/temp
41 ,name=demo
42 ,outds=work.info
43 )
44
45 Wait for it to finish, and grab the uri:
46
47 data _null_;
48 set work.info;
49 if method='GET' and rel='self';
50 call symputx('uri',uri);
51 run;
52
53 Finally, fetch the result (In this case, WEBOUT):
54
55 %mv_getjobresult(uri=&uri,result=WEBOUT_JSON,outref=myweb,outlib=myweblib)
56
57
58 @param [in] access_token_var= The global macro variable containing the access
59 token
60 @param [in] mdebug= set to 1 to enable DEBUG messages
61 @param [in] grant_type= valid values:
62 @li password
63 @li authorization_code
64 @li detect - will check if access_token exists, if not will use sas_services
65 if a SASStudioV session else authorization_code. Default option.
66 @li sas_services - will use oauth_bearer=sas_services.
67 @param [in] uri= The uri of the running job for which to fetch the status,
68 in the format `/jobExecution/jobs/$UUID` (unquoted).
69
70 @param [out] result= (WEBOUT_JSON) The result type to capture. Resolves
71 to "_[column name]" from the results table when parsed with the JSON libname
72 engine. Example values:
73 @li WEBOUT_JSON
74 @li WEBOUT_TXT
75 A job whose webout is a binary file registers it under its own name -
76 `_webout.pdf`, `_webout.xlsx` and so on - and a dotted key cannot be
77 requested through this parameter. When the requested member is absent the
78 macro falls back to the first `_WEBOUT*` member of the results that holds a
79 files uri, so a binary webout is returned without the caller naming it.
80
81 @param [out] outref= (0) The output fileref to which to write the results
82 @param [out] outlib= (0) The output library to which to assign the results
83 (assumes the data is in JSON format)
84
85
86 @version VIYA V.03.05
87 @author Allan Bowe, source: https://github.com/sasjs/core
88
89 <h4> SAS Macros </h4>
90 @li mp_abort.sas
91 @li mp_binarycopy.sas
92 @li mp_replace.sas
93 @li mf_getplatform.sas
94 @li mf_existfileref.sas
95 @li mf_getuniquefileref.sas
96 @li mf_getuniquelibref.sas
97
98**/
99
100%macro mv_getjobresult(uri=0
101 ,access_token_var=ACCESS_TOKEN
102 ,grant_type=sas_services
103 ,mdebug=0
104 ,result=WEBOUT_JSON
105 ,outref=0
106 ,outlib=0
107 );
108%if &mdebug=1 %then %do;
109 %put &sysmacroname entry vars:;
110 %put _local_;
111%end;
112
113%local oauth_bearer;
114%if &grant_type=detect %then %do;
115 %if %symexist(&access_token_var) %then %let grant_type=authorization_code;
116 %else %let grant_type=sas_services;
117%end;
118%if &grant_type=sas_services %then %do;
119 %let oauth_bearer=oauth_bearer=sas_services;
120 %let &access_token_var=;
121%end;
122
123%mp_abort(iftrue=(&grant_type ne authorization_code and &grant_type ne password
124 and &grant_type ne sas_services
125 )
126 ,mac=&sysmacroname
127 ,msg=%str(Invalid value for grant_type: &grant_type)
128)
129
130
131/* validation in datastep for better character safety */
132%local errmsg errflg;
133data _null_;
134 uri=symget('uri');
135 if length(uri)<12 then do;
136 call symputx('errflg',1);
137 call symputx('errmsg',"URI is invalid (too short) - '&uri'",'l');
138 end;
139 if scan(uri,-1)='state' or scan(uri,1) ne 'jobExecution' then do;
140 call symputx('errflg',1);
141 call symputx('errmsg',
142 "URI should be in format /jobExecution/jobs/$$$$UUID$$$$"
143 !!" but is actually like: &uri",'l');
144 end;
145run;
146
147%mp_abort(iftrue=(&errflg=1)
148 ,mac=&sysmacroname
149 ,msg=%str(&errmsg)
150)
151
152%if &outref ne 0 and %mf_existfileref(&outref) ne 1 %then %do;
153 filename &outref temp;
154%end;
155
156options noquotelenmax;
157%local base_uri; /* location of rest apis */
158%let base_uri=%mf_getplatform(VIYARESTAPI);
159
160/* fetch job info */
161%local fname1;
162%let fname1=%mf_getuniquefileref();
163proc http method='GET' out=&fname1 &oauth_bearer
164 url="&base_uri&uri";
165 headers "Accept"="application/json"
166 %if &grant_type=authorization_code %then %do;
167 "Authorization"="Bearer &&&access_token_var"
168 %end;
169 ;
170run;
171%if &SYS_PROCHTTP_STATUS_CODE ne 200 and &SYS_PROCHTTP_STATUS_CODE ne 201 %then
172%do;
173 data _null_;infile &fname1;input;putlog _infile_;run;
174 %mp_abort(mac=&sysmacroname
175 ,msg=%str(&SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE)
176 )
177%end;
178%if &mdebug=1 %then %do;
179 data _null_;
180 infile &fname1 lrecl=32767;
181 input;
182 putlog _infile_;
183 run;
184%end;
185
186/* extract results link */
187%local lib1 resuri resname resfall want;
188%let resfall=0;
189%let want=%upcase(_&result);
190%let lib1=%mf_getuniquelibref();
191libname &lib1 JSON fileref=&fname1;
192/* The member is located by name rather than referenced by name: a results
193 * table with no such column cannot be addressed in a data step at all. A
194 * binary webout registers itself as _webout.pdf / .xlsx / .zip, which
195 * result= cannot express, so when the requested member is missing the first
196 * _WEBOUT* column holding a files uri is taken instead - a job that
197 * registered more than one binary member cannot steer this, so only one
198 * binary member per job is supported. */
199data _null_;
200 set &lib1..results;
201 array _chars{*} _character_;
202 length _name $32 _val $512;
203 do _i=1 to dim(_chars);
204 _name=vname(_chars{_i});
205 if upcase(_name)=symget('want') then do;
206 _val=_chars{_i};
207 leave;
208 end;
209 end;
210 if _val eq '' then do _i=1 to dim(_chars);
211 _name=vname(_chars{_i});
212 if _chars{_i}=:'/files/' and upcase(_name)=:'_WEBOUT' then do;
213 _val=_chars{_i};
214 call symputx('resfall',1,'l');
215 leave;
216 end;
217 end;
218 if _val ne '' then do;
219 call symputx('resuri',_val,'l');
220 call symputx('resname',_name,'l');
221 end;
222 %if &mdebug=1 %then %do;
223 putlog "&sysmacroname results: " (_all_)(=);
224 %end;
225run;
226%if &mdebug=1 and &resfall=1 %then %do;
227 %put &sysmacroname: _&result was not registered, using &resname;
228%end;
229%if "%superq(resuri)" eq "" or "%superq(resuri)" eq "." %then %do;
230 /* The requested result was not registered - typical for a canceled
231 * job whose session ended before the result file was created (eg a
232 * job aborted by design, or a job that failed on session creation).
233 * Aborting here would terminate the calling program, so write an
234 * empty payload and return - the caller can check the job state
235 * (from the outds of the wait macro) and decide what to do. */
236 %put &sysmacroname: _&result not found in job results -;
237 %put &sysmacroname: use mv_getjoblog to fetch the job log instead;
238 %if &outref ne 0 %then %do;
239 data _null_;
240 file &outref;
241 put "&sysmacroname: no &_result result available for job &uri";
242 run;
243 %end;
244 %if &outlib ne 0 %then %do;
245 %local fname3;
246 %let fname3=%mf_getuniquefileref();
247 data _null_;
248 file &fname3;
249 put '{}';
250 run;
251 libname &outlib JSON fileref=&fname3;
252 %end;
253 %if &mdebug=0 %then %do;
254 filename &fname1 clear;
255 libname &lib1 clear;
256 %end;
257 %return;
258%end;
259
260/* extract results */
261%local fname2;
262%let fname2=%mf_getuniquefileref();
263proc http method='GET' out=&fname2 &oauth_bearer
264 url="&base_uri&resuri/content?limit=10000";
265 headers "Accept"="application/json"
266 %if &grant_type=authorization_code %then %do;
267 "Authorization"="Bearer &&&access_token_var"
268 %end;
269 ;
270run;
271%if &mdebug=1 %then %do;
272 /* send one char at a time as the json can be very wide */
273 data _null_;
274 infile &fname2 recfm=n;
275 input char $char1. ;
276 putlog char $char1. @;
277 run;
278%end;
279
280%if &outref ne 0 %then %do;
281 filename &outref temp;
282 %mp_binarycopy(inref=&fname2,outref=&outref)
283%end;
284%if &outlib ne 0 %then %do;
285 /* A service run through Viya job execution wraps its response in the
286 * transport envelope (>>weboutBEGIN<< ... >>weboutEND<<) - that is what the
287 * client frames the response with. The text path in mx_execute strips it,
288 * but the JSON engine has no such step and rejects the marker on line one,
289 * which is why an aborted child's result looked unparsable when the abort
290 * object inside it was perfectly good.
291 *
292 * Detection is the strip: mp_replace copies a byte at a time, so the markers
293 * come out wherever they sit, and a smaller output than input is the answer
294 * to "was there an envelope". Probing the first record instead would miss a
295 * marker that is not in it, which is the failure this exists to remove,
296 * and a result without an envelope keeps the direct libname. */
297 %local path2 path3 begvar endvar nil fref3 fref4 fid size2 size4;
298 %let fref3=%mf_getuniquefileref();
299 %let fref4=%mf_getuniquefileref();
300 %let path2=%sysfunc(pathname(&fname2));
301 %let path3=%sysfunc(pathname(&fref3));
302 %let begvar=%str(>>weboutBEGIN<<);
303 %let endvar=%str(>>weboutEND<<);
304 %let nil=;
305 %mp_replace(infile="&path2",findvar=begvar,replacevar=nil
306 ,outfile="&path3")
307 %mp_replace(infile="&path3",findvar=endvar,replacevar=nil
308 ,outfile="%sysfunc(pathname(&fref4))")
309 /* a copy that could not be written is not an outcome: say so, rather than
310 * handing the engine an empty file and failing at the libname instead */
311 %if %sysfunc(fexist(&fref4)) ne 1 %then %do;
312 %mp_abort(mac=&sysmacroname
313 ,msg=%str(Unable to strip the result envelope for &uri)
314 )
315 %end;
316 %let fid=%sysfunc(fopen(&fname2));
317 %let size2=%sysfunc(finfo(&fid,File Size (bytes)));
318 %let fid=%sysfunc(fclose(&fid));
319 %let fid=%sysfunc(fopen(&fref4));
320 %let size4=%sysfunc(finfo(&fid,File Size (bytes)));
321 %let fid=%sysfunc(fclose(&fid));
322 %if &size4 < &size2 %then %do;
323 /* nothing but envelope is a real outcome - a service that aborted before
324 * writing anything - and becomes the empty object the not-found branch
325 * hands back, rather than an empty file the engine cannot open */
326 %if &size4=0 %then %do;
327 data _null_;
328 file &fref4;
329 put '{}';
330 run;
331 %end;
332 libname &outlib JSON fileref=&fref4;
333 %end;
334 %else %do;
335 libname &outlib JSON fileref=&fname2;
336 %end;
337 filename &fref3 clear;
338 filename &fref4 clear;
339%end;
340
341%if &mdebug=0 %then %do;
342 filename &fname1 clear;
343 filename &fname2 clear;
344 libname &lib1 clear;
345%end;
346%else %do;
347 %put &sysmacroname exit vars:;
348 %put _local_;
349%end;
350
351%mend mv_getjobresult;