class FHIRClient(object): """ Instances of this class handle authorizing and talking to SMART on FHIR servers. The settings dictionary supports: - `app_id`*: Your app/client-id, e.g. 'my_web_app' - `app_secret`*: Your app/client-secret - `api_base`*: The FHIR service to connect to, e.g. 'https://fhir-api-dstu2.smarthealthit.org' - `redirect_uri`: The callback/redirect URL for your app, e.g. 'http://localhost:8000/fhir-app/' when testing locally - `patient_id`: The patient id against which to operate, if already known - `scope`: Space-separated list of scopes to request, if other than default - `launch_token`: The launch token """ def __init__(self, settings=None, state=None, save_func=lambda x: x): self.app_id = None self.app_secret = None """ The app-id for the app this client is used in. """ self.server = None self.scope = scope_default self.redirect = None """ The redirect-uri that will be used to redirect after authorization. """ self.launch_token = None """ The token/id provided at launch, if any. """ self.launch_context = None """ Context parameters supplied by the server during launch. """ self.wants_patient = True """ If true and launched without patient, will add the correct scope to indicate that the server should prompt for a patient after login. """ self.patient_id = None self._patient = None if save_func is None: raise Exception( "Must supply a save_func when initializing the SMART client") self._save_func = save_func # init from state if state is not None: self.from_state(state) # init from settings dict elif settings is not None: if not 'app_id' in settings: raise Exception("Must provide 'app_id' in settings dictionary") if not 'api_base' in settings: raise Exception( "Must provide 'api_base' in settings dictionary") self.app_id = settings['app_id'] self.app_secret = settings.get('app_secret') self.redirect = settings.get('redirect_uri') self.patient_id = settings.get('patient_id') self.scope = settings.get('scope', self.scope) self.launch_token = settings.get('launch_token') self.server = FHIRServer(self, base_uri=settings['api_base']) else: raise Exception( "Must either supply settings or a state upon client initialization" ) # MARK: Authorization @property def desired_scope(self): """ Ensures `self.scope` is completed with launch scopes, according to current client settings. """ scope = self.scope if self.launch_token is not None: scope = ' '.join([scope_haslaunch, scope]) elif self.patient_id is None and self.wants_patient: scope = ' '.join([scope_patientlaunch, scope]) return scope @property def ready(self): """ Returns True if the client is ready to make API calls (e.g. there is an access token or this is an open server). :returns: True if the server can make authenticated calls """ return self.server.ready if self.server is not None else False def prepare(self): """ Returns True if the client is ready to make API calls (e.g. there is an access token or this is an open server). In contrast to the `ready` property, this method will fetch the server's capability statement if it hasn't yet been fetched. :returns: True if the server can make authenticated calls """ if self.server: if self.server.ready: return True return self.server.prepare() return False @property def authorize_url(self): """ The URL to use to receive an authorization token. """ return self.server.authorize_uri if self.server is not None else None def handle_callback(self, url): """ You can call this to have the client automatically handle the auth callback after the user has logged in. :param str url: The complete callback URL """ ctx = self.server.handle_callback( url) if self.server is not None else None self._handle_launch_context(ctx) def reauthorize(self): """ Try to reauthorize with the server. :returns: A bool indicating reauthorization success """ ctx = self.server.reauthorize() if self.server is not None else None self._handle_launch_context(ctx) return self.launch_context is not None def _handle_launch_context(self, ctx): logger.debug("SMART: Handling launch context: {0}".format(ctx)) if 'patient' in ctx: #print('Patient id was {0}, row context is {1}'.format(self.patient_id, ctx)) self.patient_id = ctx['patient'] # TODO: TEST THIS! if 'id_token' in ctx: logger.warning("SMART: Received an id_token, ignoring") self.launch_context = ctx self.save_state() # MARK: Current Patient @property def patient(self): if self._patient is None and self.patient_id is not None and self.ready: import models.patient try: logger.debug("SMART: Attempting to read Patient {0}".format( self.patient_id)) self._patient = models.patient.Patient.read( self.patient_id, self.server) except FHIRUnauthorizedException as e: if self.reauthorize(): logger.debug( "SMART: Attempting to read Patient {0} after reauthorizing" .format(self.patient_id)) self._patient = models.patient.Patient.read( self.patient_id, self.server) except FHIRNotFoundException as e: logger.warning("SMART: Patient with id {0} not found".format( self.patient_id)) self.patient_id = None self.save_state() return self._patient def human_name(self, human_name_instance): """ Formats a `HumanName` instance into a string. """ if human_name_instance is None: return 'Unknown' parts = [] for n in [human_name_instance.prefix, human_name_instance.given]: if n is not None: parts.extend(n) if human_name_instance.family: parts.append(human_name_instance.family) if human_name_instance.suffix and len(human_name_instance.suffix) > 0: if len(parts) > 0: parts[len(parts) - 1] = parts[len(parts) - 1] + ',' parts.extend(human_name_instance.suffix) return ' '.join(parts) if len(parts) > 0 else 'Unnamed' # MARK: State def reset_patient(self): self.launch_token = None self.launch_context = None self.patient_id = None self._patient = None self.save_state() @property def state(self): return { 'app_id': self.app_id, 'app_secret': self.app_secret, 'scope': self.scope, 'redirect': self.redirect, 'patient_id': self.patient_id, 'server': self.server.state, 'launch_token': self.launch_token, 'launch_context': self.launch_context, } def from_state(self, state): assert state self.app_id = state.get('app_id') or self.app_id self.app_secret = state.get('app_secret') or self.app_secret self.scope = state.get('scope') or self.scope self.redirect = state.get('redirect') or self.redirect self.patient_id = state.get('patient_id') or self.patient_id self.launch_token = state.get('launch_token') or self.launch_token self.launch_context = state.get( 'launch_context') or self.launch_context self.server = FHIRServer(self, state=state.get('server')) def save_state(self): self._save_func(self.state)
class FHIRClient(object): """ Instances of this class handle authorizing and talking to SMART on FHIR servers. The settings dictionary supports: - `app_id`*: Your app/client-id, e.g. 'my_web_app' - `api_base`*: The FHIR service to connect to, e.g. 'https://fhir-api-dstu2.smarthealthit.org' - `redirect_uri`: The callback/redirect URL for your app, e.g. 'http://localhost:8000/fhir-app/' when testing locally - `patient_id`: The patient id against which to operate, if already known - `scope`: Space-separated list of scopes to request, if other than default - `launch_token`: The launch token """ def __init__(self, settings=None, state=None, save_func=lambda x:x): self.app_id = None """ The app-id for the app this client is used in. """ self.server = None self.scope = scope_default self.redirect = None """ The redirect-uri that will be used to redirect after authorization. """ self.launch_token = None """ The token/id provided at launch, if any. """ self.launch_context = None """ Context parameters supplied by the server during launch. """ self.wants_patient = True """ If true and launched without patient, will add the correct scope to indicate that the server should prompt for a patient after login. """ self.patient_id = None self._patient = None if save_func is None: raise Exception("Must supply a save_func when initializing the SMART client") self._save_func = save_func # init from state if state is not None: self.from_state(state) # init from settings dict elif settings is not None: if not 'app_id' in settings: raise Exception("Must provide 'app_id' in settings dictionary") if not 'api_base' in settings: raise Exception("Must provide 'api_base' in settings dictionary") self.app_id = settings['app_id'] self.redirect = settings.get('redirect_uri') self.patient_id = settings.get('patient_id') self.scope = settings.get('scope', self.scope) self.launch_token = settings.get('launch_token') self.server = FHIRServer(self, base_uri=settings['api_base']) else: raise Exception("Must either supply settings or a state upon client initialization") # MARK: Authorization @property def desired_scope(self): """ Ensures `self.scope` is completed with launch scopes, according to current client settings. """ scope = self.scope if self.launch_token is not None: scope = ' '.join([scope_haslaunch, scope]) elif self.patient_id is None and self.wants_patient: scope = ' '.join([scope_patientlaunch, scope]) return scope @property def ready(self): """ Returns True if the client is ready to make API calls (e.g. there is an access token or this is an open server). :returns: True if the server can make authenticated calls """ return self.server.ready if self.server is not None else False def prepare(self): """ Returns True if the client is ready to make API calls (e.g. there is an access token or this is an open server). In contrast to the `ready` property, this method will fetch the server's Conformance statement if it hasn't yet been fetched. :returns: True if the server can make authenticated calls """ if self.server: if self.server.ready: return True return self.server.prepare() return False @property def authorize_url(self): """ The URL to use to receive an authorization token. """ return self.server.authorize_uri if self.server is not None else None def handle_callback(self, url): """ You can call this to have the client automatically handle the auth callback after the user has logged in. :param str url: The complete callback URL """ ctx = self.server.handle_callback(url) if self.server is not None else None self._handle_launch_context(ctx) def reauthorize(self): """ Try to reauthorize with the server. :returns: A bool indicating reauthorization success """ ctx = self.server.reauthorize(self.server) if self.server is not None else None self._handle_launch_context(ctx) return self.launch_context is not None def _handle_launch_context(self, ctx): logging.debug("SMART: Handling launch context: {0}".format(ctx)) if 'patient' in ctx: #print('Patient id was {0}, row context is {1}'.format(self.patient_id, ctx)) self.patient_id = ctx['patient'] # TODO: TEST THIS! if 'id_token' in ctx: logging.warning("SMART: Received an id_token, ignoring") self.launch_context = ctx self.save_state() # MARK: Current Patient @property def patient(self): if self._patient is None and self.patient_id is not None and self.ready: import models.patient try: logging.debug("SMART: Attempting to read Patient {0}".format(self.patient_id)) self._patient = models.patient.Patient.read(self.patient_id, self.server) except FHIRUnauthorizedException as e: if self.reauthorize(): logging.debug("SMART: Attempting to read Patient {0} after reauthorizing" .format(self.patient_id)) self._patient = models.patient.Patient.read(self.patient_id, self.server) except FHIRNotFoundException as e: logging.warning("SMART: Patient with id {0} not found".format(self.patient_id)) self.patient_id = None self.save_state() return self._patient def human_name(self, human_name_instance): """ Formats a `HumanName` instance into a string. """ if human_name_instance is None: return 'Unknown' parts = [] for n in [human_name_instance.prefix, human_name_instance.given, human_name_instance.family, human_name_instance.suffix]: if n is not None: parts.extend(n) return ' '.join(parts) if len(parts) > 0 else 'Unnamed' # MARK: State def reset_patient(self): self.launch_token = None self.launch_context = None self.patient_id = None self._patient = None self.save_state() @property def state(self): return { 'app_id': self.app_id, 'scope': self.scope, 'redirect': self.redirect, 'patient_id': self.patient_id, 'server': self.server.state, 'launch_token': self.launch_token, 'launch_context': self.launch_context, } def from_state(self, state): assert state self.app_id = state.get('app_id') or self.app_id self.scope = state.get('scope') or self.scope self.redirect = state.get('redirect') or self.redirect self.patient_id = state.get('patient_id') or self.patient_id self.launch_token = state.get('launch_token') or self.launch_token self.launch_context = state.get('launch_context') or self.launch_context self.server = FHIRServer(self, state=state.get('server')) def save_state (self): self._save_func(self.state)