Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# -*- coding: utf-8 -*- 

2 

3import logging 

4 

5from server import FHIRServer, FHIRUnauthorizedException, FHIRNotFoundException 

6 

7__version__ = '4.0.0' 

8__author__ = 'SMART Platforms Team' 

9__license__ = 'APACHE2' 

10__copyright__ = "Copyright 2017 Boston Children's Hospital" 

11 

12scope_default = 'user/*.* patient/*.read openid profile' 

13scope_haslaunch = 'launch' 

14scope_patientlaunch = 'launch/patient' 

15 

16logger = logging.getLogger(__name__) 

17 

18 

19class FHIRClient(object): 

20 """ Instances of this class handle authorizing and talking to SMART on FHIR 

21 servers. 

22  

23 The settings dictionary supports: 

24  

25 - `app_id`*: Your app/client-id, e.g. 'my_web_app' 

26 - `app_secret`*: Your app/client-secret 

27 - `api_base`*: The FHIR service to connect to, e.g. 'https://fhir-api-dstu2.smarthealthit.org' 

28 - `redirect_uri`: The callback/redirect URL for your app, e.g. 'http://localhost:8000/fhir-app/' when testing locally 

29 - `patient_id`: The patient id against which to operate, if already known 

30 - `scope`: Space-separated list of scopes to request, if other than default 

31 - `launch_token`: The launch token 

32 """ 

33 

34 def __init__(self, settings=None, state=None, save_func=lambda x:x): 

35 self.app_id = None 

36 self.app_secret = None 

37 """ The app-id for the app this client is used in. """ 

38 

39 self.server = None 

40 self.scope = scope_default 

41 self.redirect = None 

42 """ The redirect-uri that will be used to redirect after authorization. """ 

43 

44 self.launch_token = None 

45 """ The token/id provided at launch, if any. """ 

46 

47 self.launch_context = None 

48 """ Context parameters supplied by the server during launch. """ 

49 

50 self.wants_patient = True 

51 """ If true and launched without patient, will add the correct scope 

52 to indicate that the server should prompt for a patient after login. """ 

53 

54 self.patient_id = None 

55 self._patient = None 

56 

57 if save_func is None: 

58 raise Exception("Must supply a save_func when initializing the SMART client") 

59 self._save_func = save_func 

60 

61 # init from state 

62 if state is not None: 

63 self.from_state(state) 

64 

65 # init from settings dict 

66 elif settings is not None: 

67 if not 'app_id' in settings: 

68 raise Exception("Must provide 'app_id' in settings dictionary") 

69 if not 'api_base' in settings: 

70 raise Exception("Must provide 'api_base' in settings dictionary") 

71 

72 self.app_id = settings['app_id'] 

73 self.app_secret = settings.get('app_secret') 

74 self.redirect = settings.get('redirect_uri') 

75 self.patient_id = settings.get('patient_id') 

76 self.scope = settings.get('scope', self.scope) 

77 self.launch_token = settings.get('launch_token') 

78 self.server = FHIRServer(self, base_uri=settings['api_base']) 

79 else: 

80 raise Exception("Must either supply settings or a state upon client initialization") 

81 

82 

83 # MARK: Authorization 

84 

85 @property 

86 def desired_scope(self): 

87 """ Ensures `self.scope` is completed with launch scopes, according to 

88 current client settings. 

89 """ 

90 scope = self.scope 

91 if self.launch_token is not None: 

92 scope = ' '.join([scope_haslaunch, scope]) 

93 elif self.patient_id is None and self.wants_patient: 

94 scope = ' '.join([scope_patientlaunch, scope]) 

95 return scope 

96 

97 @property 

98 def ready(self): 

99 """ Returns True if the client is ready to make API calls (e.g. there 

100 is an access token or this is an open server). 

101  

102 :returns: True if the server can make authenticated calls 

103 """ 

104 return self.server.ready if self.server is not None else False 

105 

106 def prepare(self): 

107 """ Returns True if the client is ready to make API calls (e.g. there 

108 is an access token or this is an open server). In contrast to the 

109 `ready` property, this method will fetch the server's capability 

110 statement if it hasn't yet been fetched. 

111  

112 :returns: True if the server can make authenticated calls 

113 """ 

114 if self.server: 

115 if self.server.ready: 

116 return True 

117 return self.server.prepare() 

118 return False 

119 

120 @property 

121 def authorize_url(self): 

122 """ The URL to use to receive an authorization token. 

123 """ 

124 return self.server.authorize_uri if self.server is not None else None 

125 

126 def handle_callback(self, url): 

127 """ You can call this to have the client automatically handle the 

128 auth callback after the user has logged in. 

129  

130 :param str url: The complete callback URL 

131 """ 

132 ctx = self.server.handle_callback(url) if self.server is not None else None 

133 self._handle_launch_context(ctx) 

134 

135 def reauthorize(self): 

136 """ Try to reauthorize with the server. 

137  

138 :returns: A bool indicating reauthorization success 

139 """ 

140 ctx = self.server.reauthorize() if self.server is not None else None 

141 self._handle_launch_context(ctx) 

142 return self.launch_context is not None 

143 

144 def _handle_launch_context(self, ctx): 

145 logger.debug("SMART: Handling launch context: {0}".format(ctx)) 

146 if 'patient' in ctx: 

147 #print('Patient id was {0}, row context is {1}'.format(self.patient_id, ctx)) 

148 self.patient_id = ctx['patient'] # TODO: TEST THIS! 

149 if 'id_token' in ctx: 

150 logger.warning("SMART: Received an id_token, ignoring") 

151 self.launch_context = ctx 

152 self.save_state() 

153 

154 

155 # MARK: Current Patient 

156 

157 @property 

158 def patient(self): 

159 if self._patient is None and self.patient_id is not None and self.ready: 

160 import models.patient 

161 try: 

162 logger.debug("SMART: Attempting to read Patient {0}".format(self.patient_id)) 

163 self._patient = models.patient.Patient.read(self.patient_id, self.server) 

164 except FHIRUnauthorizedException as e: 

165 if self.reauthorize(): 

166 logger.debug("SMART: Attempting to read Patient {0} after reauthorizing" 

167 .format(self.patient_id)) 

168 self._patient = models.patient.Patient.read(self.patient_id, self.server) 

169 except FHIRNotFoundException as e: 

170 logger.warning("SMART: Patient with id {0} not found".format(self.patient_id)) 

171 self.patient_id = None 

172 self.save_state() 

173 

174 return self._patient 

175 

176 def human_name(self, human_name_instance): 

177 """ Formats a `HumanName` instance into a string. 

178 """ 

179 if human_name_instance is None: 

180 return 'Unknown' 

181 

182 parts = [] 

183 for n in [human_name_instance.prefix, human_name_instance.given]: 

184 if n is not None: 

185 parts.extend(n) 

186 if human_name_instance.family: 

187 parts.append(human_name_instance.family) 

188 if human_name_instance.suffix and len(human_name_instance.suffix) > 0: 

189 if len(parts) > 0: 

190 parts[len(parts)-1] = parts[len(parts)-1]+',' 

191 parts.extend(human_name_instance.suffix) 

192 

193 return ' '.join(parts) if len(parts) > 0 else 'Unnamed' 

194 

195 

196 # MARK: State 

197 

198 def reset_patient(self): 

199 self.launch_token = None 

200 self.launch_context = None 

201 self.patient_id = None 

202 self._patient = None 

203 self.save_state() 

204 

205 @property 

206 def state(self): 

207 return { 

208 'app_id': self.app_id, 

209 'app_secret': self.app_secret, 

210 'scope': self.scope, 

211 'redirect': self.redirect, 

212 'patient_id': self.patient_id, 

213 'server': self.server.state, 

214 'launch_token': self.launch_token, 

215 'launch_context': self.launch_context, 

216 } 

217 

218 def from_state(self, state): 

219 assert state 

220 self.app_id = state.get('app_id') or self.app_id 

221 self.app_secret = state.get('app_secret') or self.app_secret 

222 self.scope = state.get('scope') or self.scope 

223 self.redirect = state.get('redirect') or self.redirect 

224 self.patient_id = state.get('patient_id') or self.patient_id 

225 self.launch_token = state.get('launch_token') or self.launch_token 

226 self.launch_context = state.get('launch_context') or self.launch_context 

227 self.server = FHIRServer(self, state=state.get('server')) 

228 

229 def save_state (self): 

230 self._save_func(self.state) 

231