Coverage for /home/martinb/workspace/client-py/fhirclient/client.py : 40%

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 -*-
3import logging
5from server import FHIRServer, FHIRUnauthorizedException, FHIRNotFoundException
7__version__ = '4.0.0'
8__author__ = 'SMART Platforms Team'
9__license__ = 'APACHE2'
10__copyright__ = "Copyright 2017 Boston Children's Hospital"
12scope_default = 'user/*.* patient/*.read openid profile'
13scope_haslaunch = 'launch'
14scope_patientlaunch = 'launch/patient'
16logger = logging.getLogger(__name__)
19class FHIRClient(object):
20 """ Instances of this class handle authorizing and talking to SMART on FHIR
21 servers.
23 The settings dictionary supports:
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 """
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. """
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. """
44 self.launch_token = None
45 """ The token/id provided at launch, if any. """
47 self.launch_context = None
48 """ Context parameters supplied by the server during launch. """
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. """
54 self.patient_id = None
55 self._patient = None
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
61 # init from state
62 if state is not None:
63 self.from_state(state)
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")
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")
83 # MARK: Authorization
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
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).
102 :returns: True if the server can make authenticated calls
103 """
104 return self.server.ready if self.server is not None else False
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.
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
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
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.
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)
135 def reauthorize(self):
136 """ Try to reauthorize with the server.
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
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()
155 # MARK: Current Patient
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()
174 return self._patient
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'
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)
193 return ' '.join(parts) if len(parts) > 0 else 'Unnamed'
196 # MARK: State
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()
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 }
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'))
229 def save_state (self):
230 self._save_func(self.state)