diff --git a/docs/.gitignore b/docs/.gitignore deleted file mode 100644 index 45c1505..0000000 --- a/docs/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -_site -.sass-cache -.jekyll-metadata diff --git a/docs/404.html b/docs/404.html deleted file mode 100644 index c472b4e..0000000 --- a/docs/404.html +++ /dev/null @@ -1,24 +0,0 @@ ---- -layout: default ---- - - - -
-

404

- -

Page not found :(

-

The requested page could not be found.

-
diff --git a/docs/Gemfile b/docs/Gemfile deleted file mode 100644 index 9a776ce..0000000 --- a/docs/Gemfile +++ /dev/null @@ -1,27 +0,0 @@ -source "https://rubygems.org" - -# Hello! This is where you manage which Jekyll version is used to run. -# When you want to use a different version, change it below, save the -# file and run `bundle install`. Run Jekyll with `bundle exec`, like so: -# -# bundle exec jekyll serve -# -# This will help ensure the proper Jekyll version is running. -# Happy Jekylling! -# gem "jekyll", "3.5.2" - -# This is the default theme for new Jekyll sites. You may change this to anything you like. -gem "minima", "~> 2.0" - -# If you want to use GitHub Pages, remove the "gem "jekyll"" above and -# uncomment the line below. To upgrade, run `bundle update github-pages`. -gem "github-pages", group: :jekyll_plugins - -# If you have any plugins, put them here! -group :jekyll_plugins do - gem "jekyll-feed", "~> 0.6" -end - -# Windows does not include zoneinfo files, so bundle the tzinfo-data gem -gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] - diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock deleted file mode 100644 index e88fbb4..0000000 --- a/docs/Gemfile.lock +++ /dev/null @@ -1,221 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - activesupport (4.2.8) - i18n (~> 0.7) - minitest (~> 5.1) - thread_safe (~> 0.3, >= 0.3.4) - tzinfo (~> 1.1) - addressable (2.5.2) - public_suffix (>= 2.0.2, < 4.0) - coffee-script (2.4.1) - coffee-script-source - execjs - coffee-script-source (1.11.1) - colorator (1.1.0) - ethon (0.10.1) - ffi (>= 1.3.0) - execjs (2.7.0) - faraday (0.13.1) - multipart-post (>= 1.2, < 3) - ffi (1.9.18) - forwardable-extended (2.6.0) - gemoji (3.0.0) - github-pages (160) - activesupport (= 4.2.8) - github-pages-health-check (= 1.3.5) - jekyll (= 3.5.2) - jekyll-avatar (= 0.5.0) - jekyll-coffeescript (= 1.0.2) - jekyll-default-layout (= 0.1.4) - jekyll-feed (= 0.9.2) - jekyll-gist (= 1.4.1) - jekyll-github-metadata (= 2.9.3) - jekyll-mentions (= 1.2.0) - jekyll-optional-front-matter (= 0.2.0) - jekyll-paginate (= 1.1.0) - jekyll-readme-index (= 0.1.0) - jekyll-redirect-from (= 0.12.1) - jekyll-relative-links (= 0.4.1) - jekyll-sass-converter (= 1.5.0) - jekyll-seo-tag (= 2.3.0) - jekyll-sitemap (= 1.1.1) - jekyll-swiss (= 0.4.0) - jekyll-theme-architect (= 0.1.0) - jekyll-theme-cayman (= 0.1.0) - jekyll-theme-dinky (= 0.1.0) - jekyll-theme-hacker (= 0.1.0) - jekyll-theme-leap-day (= 0.1.0) - jekyll-theme-merlot (= 0.1.0) - jekyll-theme-midnight (= 0.1.0) - jekyll-theme-minimal (= 0.1.0) - jekyll-theme-modernist (= 0.1.0) - jekyll-theme-primer (= 0.5.2) - jekyll-theme-slate (= 0.1.0) - jekyll-theme-tactile (= 0.1.0) - jekyll-theme-time-machine (= 0.1.0) - jekyll-titles-from-headings (= 0.4.0) - jemoji (= 0.8.0) - kramdown (= 1.13.2) - liquid (= 4.0.0) - listen (= 3.0.6) - mercenary (~> 0.3) - minima (= 2.1.1) - rouge (= 1.11.1) - terminal-table (~> 1.4) - github-pages-health-check (1.3.5) - addressable (~> 2.3) - net-dns (~> 0.8) - octokit (~> 4.0) - public_suffix (~> 2.0) - typhoeus (~> 0.7) - html-pipeline (2.7.0) - activesupport (>= 2) - nokogiri (>= 1.4) - i18n (0.8.6) - jekyll (3.5.2) - addressable (~> 2.4) - colorator (~> 1.0) - jekyll-sass-converter (~> 1.0) - jekyll-watch (~> 1.1) - kramdown (~> 1.3) - liquid (~> 4.0) - mercenary (~> 0.3.3) - pathutil (~> 0.9) - rouge (~> 1.7) - safe_yaml (~> 1.0) - jekyll-avatar (0.5.0) - jekyll (~> 3.0) - jekyll-coffeescript (1.0.2) - coffee-script (~> 2.2) - coffee-script-source (~> 1.11.1) - jekyll-default-layout (0.1.4) - jekyll (~> 3.0) - jekyll-feed (0.9.2) - jekyll (~> 3.3) - jekyll-gist (1.4.1) - octokit (~> 4.2) - jekyll-github-metadata (2.9.3) - jekyll (~> 3.1) - octokit (~> 4.0, != 4.4.0) - jekyll-mentions (1.2.0) - activesupport (~> 4.0) - html-pipeline (~> 2.3) - jekyll (~> 3.0) - jekyll-optional-front-matter (0.2.0) - jekyll (~> 3.0) - jekyll-paginate (1.1.0) - jekyll-readme-index (0.1.0) - jekyll (~> 3.0) - jekyll-redirect-from (0.12.1) - jekyll (~> 3.3) - jekyll-relative-links (0.4.1) - jekyll (~> 3.3) - jekyll-sass-converter (1.5.0) - sass (~> 3.4) - jekyll-seo-tag (2.3.0) - jekyll (~> 3.3) - jekyll-sitemap (1.1.1) - jekyll (~> 3.3) - jekyll-swiss (0.4.0) - jekyll-theme-architect (0.1.0) - jekyll (~> 3.5) - jekyll-seo-tag (~> 2.0) - jekyll-theme-cayman (0.1.0) - jekyll (~> 3.5) - jekyll-seo-tag (~> 2.0) - jekyll-theme-dinky (0.1.0) - jekyll (~> 3.5) - jekyll-seo-tag (~> 2.0) - jekyll-theme-hacker (0.1.0) - jekyll (~> 3.5) - jekyll-seo-tag (~> 2.0) - jekyll-theme-leap-day (0.1.0) - jekyll (~> 3.5) - jekyll-seo-tag (~> 2.0) - jekyll-theme-merlot (0.1.0) - jekyll (~> 3.5) - jekyll-seo-tag (~> 2.0) - jekyll-theme-midnight (0.1.0) - jekyll (~> 3.5) - jekyll-seo-tag (~> 2.0) - jekyll-theme-minimal (0.1.0) - jekyll (~> 3.5) - jekyll-seo-tag (~> 2.0) - jekyll-theme-modernist (0.1.0) - jekyll (~> 3.5) - jekyll-seo-tag (~> 2.0) - jekyll-theme-primer (0.5.2) - jekyll (~> 3.5) - jekyll-github-metadata (~> 2.9) - jekyll-seo-tag (~> 2.2) - jekyll-theme-slate (0.1.0) - jekyll (~> 3.5) - jekyll-seo-tag (~> 2.0) - jekyll-theme-tactile (0.1.0) - jekyll (~> 3.5) - jekyll-seo-tag (~> 2.0) - jekyll-theme-time-machine (0.1.0) - jekyll (~> 3.5) - jekyll-seo-tag (~> 2.0) - jekyll-titles-from-headings (0.4.0) - jekyll (~> 3.3) - jekyll-watch (1.5.0) - listen (~> 3.0, < 3.1) - jemoji (0.8.0) - activesupport (~> 4.0) - gemoji (~> 3.0) - html-pipeline (~> 2.2) - jekyll (>= 3.0) - kramdown (1.13.2) - liquid (4.0.0) - listen (3.0.6) - rb-fsevent (>= 0.9.3) - rb-inotify (>= 0.9.7) - mercenary (0.3.6) - mini_portile2 (2.2.0) - minima (2.1.1) - jekyll (~> 3.3) - minitest (5.10.3) - multipart-post (2.0.0) - net-dns (0.8.0) - nokogiri (1.8.0) - mini_portile2 (~> 2.2.0) - octokit (4.7.0) - sawyer (~> 0.8.0, >= 0.5.3) - pathutil (0.14.0) - forwardable-extended (~> 2.6) - public_suffix (2.0.5) - rb-fsevent (0.10.2) - rb-inotify (0.9.10) - ffi (>= 0.5.0, < 2) - rouge (1.11.1) - safe_yaml (1.0.4) - sass (3.5.1) - sass-listen (~> 4.0.0) - sass-listen (4.0.0) - rb-fsevent (~> 0.9, >= 0.9.4) - rb-inotify (~> 0.9, >= 0.9.7) - sawyer (0.8.1) - addressable (>= 2.3.5, < 2.6) - faraday (~> 0.8, < 1.0) - terminal-table (1.8.0) - unicode-display_width (~> 1.1, >= 1.1.1) - thread_safe (0.3.6) - typhoeus (0.8.0) - ethon (>= 0.8.0) - tzinfo (1.2.3) - thread_safe (~> 0.1) - unicode-display_width (1.3.0) - -PLATFORMS - ruby - -DEPENDENCIES - github-pages - jekyll-feed (~> 0.6) - minima (~> 2.0) - tzinfo-data - -BUNDLED WITH - 1.16.0.pre.2 diff --git a/docs/_config.yml b/docs/_config.yml deleted file mode 100644 index 4f8fd28..0000000 --- a/docs/_config.yml +++ /dev/null @@ -1,43 +0,0 @@ -# Welcome to Jekyll! -# -# This config file is meant for settings that affect your whole blog, values -# which you are expected to set up once and rarely edit after that. If you find -# yourself editing this file very often, consider using Jekyll's data files -# feature for the data you need to update frequently. -# -# For technical reasons, this file is *NOT* reloaded automatically when you use -# 'bundle exec jekyll serve'. If you change this file, please restart the server process. - -# Site settings -# These are used to personalize your new site. If you look in the HTML files, -# you will see them accessed via {{ site.title }}, {{ site.email }}, and so on. -# You can create any custom variable you would like, and they will be accessible -# in the templates via {{ site.myvariable }}. -title: HealthTools.KE-api documentation -email: your-email@example.com -description: > # this means to ignore newlines until "baseurl:" - Write an awesome description for your new site here. You can edit this - line in _config.yml. It will appear in your document head meta (for - Google search results) and in your feed.xml site description. -baseurl: "" # the subpath of your site, e.g. /blog -url: "" # the base hostname & protocol for your site, e.g. http://example.com -twitter_username: jekyllrb -github_username: jekyll - -# Build settings -markdown: kramdown -theme: minima -plugins: - - jekyll-feed - -# Exclude from processing. -# The following items will not be processed, by default. Create a custom list -# to override the default setting. -# exclude: -# - Gemfile -# - Gemfile.lock -# - node_modules -# - vendor/bundle/ -# - vendor/cache/ -# - vendor/gems/ -# - vendor/ruby/ diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index 82e5944..0000000 --- a/docs/index.md +++ /dev/null @@ -1,73 +0,0 @@ ---- -# You don't need to edit this file, it's empty on purpose. -# Edit theme's home layout instead if you wanna make some changes -# See: https://jekyllrb.com/docs/themes/#overriding-theme-defaults -layout: home ---- -# HealthTools.KE-api -HealthTools Kenya API to retrieve, structure and return data being used by the health tools. It provides -data on the following medical officer registries: - -- Doctors: http://medicalboard.co.ke/online-services/retention/ -- Foreign doctors: http://medicalboard.co.ke/online-services/foreign-doctors-license-register -- Clinical officers: http://clinicalofficerscouncil.org/online-services/retention/ - -### Specifications -Specification for the API is shown below. It is an open api and requires no authentication to access. - - -| EndPoint | Allowed Methods | Functionality | Parameters | -|-------------------------------------|------------------|----------------------------------------------------------|------------| -| `/doctors/search.json` | GET | Search a doctor by the name | q=[name] | -| `/nurses/search.json` | GET | Search a nurse by the name | q=[name] | -| `/clinical-officers/search.json` | GET | Search a clinical officer by the name | q=[name] | - - -### Installation -Clone the repo from github `$ git clone git@github.com:RyanSept/HealthTools.KE-api.git` - -Change directory into package `$ cd HealthTools.KE-api` - -Install the dependencies by running `$ pip install -r requirements.txt` - -Install Memcached - * If on linux follow this [link](https://github.com/memcached/memcached/wiki/Install) - * On mac use `brew install memcached` - -You can set the required environment variables like so -```<> -$ export APP_DEBUG= # True or False -$ export MEMCACHED_URL= # defaults to 127.0.0.1:8000 -$ export GA_TRACKING_ID= -$ export SMS_USER= -$ export SMS_PASS= -$ export SMS_SHORTCODE= -$ export SMS_SEND_URL= -$ export AWS_ACCESS_KEY= -$ export AWS_SECRET_KEY= -$ export ES_HOST= (DO NOT SET THIS IF YOU WOULD LIKE TO USE ELASTIC SEARCH LOCALLY ON YOUR MACHINE) -$ export ES_PORT= -$ export ES_INDEX= -``` -**If you want to use elasticsearch locally on your machine use the following instructions to set it up** - -For linux and windows users, follow instructions from this [link](https://www.elastic.co/guide/en/elasticsearch/reference/current/setup.html) - -For mac users run `brew install elasticsearch` on your terminal - -Run memcached on your terminal `$ memcached -p (default: 8000)` - -If you set up elasticsearch locally run it `$ elasticsearch` - -You can now run the server `$ python manage.py` or `gunicorn manage:app` for production. - - - -## Running the tests - -Run memcached on your terminal `$ memcached -p (default: 8000)` - -_**make sure if you use elasticsearch locally, it's running**_ - -Use nosetests to run tests (with stdout) like this: -```$ nosetests --nocapture``` diff --git a/healthtools/analytics.py b/healthtools/analytics.py deleted file mode 100644 index e700b82..0000000 --- a/healthtools/analytics.py +++ /dev/null @@ -1,30 +0,0 @@ -import requests - - -def track_event(tracking_id, category, action, cid, label=None, value=0): - ''' - Posts Tracking in info to Google Analytics using measurement protocol. - Args: - tracking_id: The tracking ID of the Google Analytics account in which these data is associated with. - category: The name assigned to the group of similar events to track. - action: The Specific action being tracked. - cid: Anonymous Client Identifier. Ideally, this should be a UUID that is associated with particular user, device - label: Label of the event. - value: Value of event in this case number of results obtained - Returns: - No return value # If the request fails, it will raise a - RequestException. . - ''' - data = { - 'v': '1', - 'tid': tracking_id, - 'cid': cid, - 't': 'event', - 'ec': category, - 'ea': action, - 'el': label, - 'ev': value, - } - response = requests.post( - 'http://www.google-analytics.com/collect', data=data) - response.raise_for_status() diff --git a/healthtools/build_query.py b/healthtools/build_query.py deleted file mode 100644 index d980d84..0000000 --- a/healthtools/build_query.py +++ /dev/null @@ -1,258 +0,0 @@ -import getpass -import json -import requests -import re - -from datetime import datetime -from flask import Blueprint, request, current_app - -from healthtools_ke_api.analytics import track_event -from healthtools_ke_api.views.nurses import get_nurses_from_nc_registry -from healthtools_ke_api.elastic_search import Elastic - -es = Elastic() - - -class BuildQuery(object): - def __init__(self): - self.SMS_RESULT_COUNT = 4 # Number of results to be send via sms - self.DOC_KEYWORDS = ['doc', 'daktari', 'doctor', 'oncologist', 'dr'] - self.CO_KEYWORDS = ['CO', 'clinical officer', - 'clinic officer', 'clinical', 'clinical oficer', ] - self.NO_KEYWORDS = ['nurse', 'no', 'nursing officer', - 'mhuguzi', 'muuguzi', 'RN', 'Registered Nurse'] - self.NHIF_KEYWORDS = ['nhif', 'bima', 'insurance', - 'insurance fund', 'health insurance', 'hospital fund'] - self.HF_KEYWORDS = ['hf', 'hospital', 'dispensary', 'clinic', - 'hospitali', 'sanatorium', 'health centre'] - - def find_keyword_in_query(self, query, keywords): - regex = re.compile(r'\b(?:%s)\b' % '|'.join(keywords), re.IGNORECASE) - return re.search(regex, query) - - def build_query_response(self, query): - query = self.clean_query(query) - # Start by looking for doctors keywords - if self.find_keyword_in_query(query, self.DOC_KEYWORDS): - search_terms = self.find_keyword_in_query(query, self.DOC_KEYWORDS) - query = query[:search_terms.start()] + query[search_terms.end():] - print query - doctors = es.get_from_elasticsearch('doctors', query) - msg = self.construct_docs_response(doctors[:self.SMS_RESULT_COUNT]) - self.check_message(msg) - return [msg] - # Looking for Nurses keywords - elif self.find_keyword_in_query(query, self.NO_KEYWORDS): - search_terms = self.find_keyword_in_query(query, self.NO_KEYWORDS) - query = query[:search_terms.start()] + query[search_terms.end():] - nurses = get_nurses_from_nc_registry(query) - msg = self.construct_nurse_response(nurses[:self.SMS_RESULT_COUNT]) - self.check_message(msg) - return [msg] - # Looking for clinical officers Keywords - elif self.find_keyword_in_query(query, self.CO_KEYWORDS): - search_terms = self.find_keyword_in_query(query, self.CO_KEYWORDS) - query = query[:search_terms.start()] + query[search_terms.end():] - print query - clinical_officers = es.get_from_elasticsearch( - 'clinical-officers', query) - msg = self.construct_co_response( - clinical_officers[:self.SMS_RESULT_COUNT]) - self.check_message(msg) - return [msg] - # Looking for nhif hospitals - elif self.find_keyword_in_query(query, self.NHIF_KEYWORDS): - search_terms = self.find_keyword_in_query( - query, self.NHIF_KEYWORDS) - query = query[:search_terms.start()] + query[search_terms.end():] - - if re.search("inpatient", query): - query = re.sub("\s*inpatient\s*", "", - query, flags=re.IGNORECASE) - doc_type = "nhif-inpatient" - else: - query = re.sub("\s*outpatient\s*", "", - query, flags=re.IGNORECASE) - - # Default: ES doc_type = nhif-outpatient-cs - doc_type = ["nhif-outpatient", "nhif-outpatient-cs"] - - nhif_hospitals = es.get_from_elasticsearch( - doc_type, query) - - msg = self.construct_nhif_response(nhif_hospitals) - print msg - return [msg] - # Looking for health facilities - elif self.find_keyword_in_query(query, self.HF_KEYWORDS): - search_terms = self.find_keyword_in_query(query, self.HF_KEYWORDS) - query = query[:search_terms.start()] + query[search_terms.end():] - health_facilities = es.get_from_elasticsearch( - 'health-facilities', query) - msg = self.construct_hf_response( - health_facilities[:self.SMS_RESULT_COUNT]) - print msg - return [msg] - # If we miss the keywords then reply with the preferred query formats - else: - self.print_error(query) - msg_items = list() - msg_items.append("We could not understand your query. Try these:") - msg_items.append("1. Doctors: DR. SAMUEL AMAI") - msg_items.append("2. Clinical Officers: CO SAMUEL AMAI") - msg_items.append("3. Nurses: NURSE SAMUEL AMAI") - msg_items.append("4. NHIF accredited hospital: NHIF KITALE") - msg_items.append("5. Health Facility: HF KITALE") - msg = " ".join(msg_items) - return [msg, {'error': " ".join(msg_items)}] - - def construct_co_response(self, co_list): - # Just incase we found ourselves here with an empty list - if len(co_list) < 1: - return "Could not find a clinical officer with that name." - count = 1 - msg_items = [] - for co in co_list: - # co = co["fields"] - co = co["_source"] - status = " ".join( - [str(count) + ".", "".join(co['name'].title()), "-", "".join(co['qualifications'].upper())]) - msg_items.append(status) - count = count + 1 - if len(co_list) > 1: - msg_items.append( - "\nFind the full list at http://health.the-star.co.ke") - print "\n".join(msg_items) - return "\n".join(msg_items) - - def construct_docs_response(self, docs_list): - # Just incase we found ourselves here with an empty list - if len(docs_list) < 1: - return "Could not find a doctor with that name" - count = 1 - msg_items = [] - - for doc in docs_list: - doc = doc["_source"] - status = " ".join([str(count) + ".", "".join(doc['name'].title()), "-", - "".join(doc['reg_no'].title()), "-", "".join(doc['qualifications'].upper())]) - - msg_items.append(status) - count = count + 1 - if len(docs_list) > 1: - msg_items.append( - "\nFind the full list at http://health.the-star.co.ke") - - return "\n".join(msg_items) - - def construct_nurse_response(self, nurse_list): - # Just incase we found ourselves here with an empty list - if len(nurse_list) < 1: - return "Could not find a nurse with that name" - count = 1 - msg_items = [] - for nurse in nurse_list: - status = " ".join([str(count) + ".", nurse['name'].title() + - ",", "Valid to", nurse['valid_till'].title()]) - msg_items.append(status) - count = count + 1 - if len(nurse_list) > 1: - msg_items.append( - "\nFind the full list at http://health.the-star.co.ke") - - return "\n".join(msg_items) - - def construct_nhif_response(self, nhif_list): - # Just incase we found ourselves here with an empty list - if len(nhif_list) < 1: - return "We could not find an NHIF accredited hospital with the name or in the location you provided." - count = 1 - msg_items = [] - nhif_hospitals = [] - - for nhif in nhif_list: - nhif = nhif['_source'] - hospital = nhif['hospital'] - nhif_hospitals.append(hospital) - - nhif_hospitals = list(set(nhif_hospitals)) - - for hospital in nhif_hospitals: - status = " ".join([str(count) + ".", hospital.title()]) - msg_items.append(status) - count = count + 1 - - if len(nhif_list) > 1: - msg_items.append( - "\nFind the full list at http://health.the-star.co.ke") - - return "\n".join(msg_items) - - def construct_hf_response(self, hf_list): - # Just incase we found ourselves here with an empty list - if len(hf_list) < 1: - return "We could not find a health facility with the name or in the location you provided." - count = 1 - msg_items = [] - for hf in hf_list: - hf = hf['_source'] - status = " ".join([str(count) + ".", hf['name'].title() + - " -", hf['keph_level_name'].title()]) - msg_items.append(status) - count = count + 1 - if len(hf_list) > 1: - msg_items.append( - "\nFind the full list at http://health.the-star.co.ke") - - return "\n".join(msg_items) - - def clean_query(self, query): - query = query.lower().strip().replace(".", "") - return query - - def parse_elastic_search_results(self, response): - result_to_send_count = self.SMS_RESULT_COUNT - data_dict = response.json() - fields_dict = (data_dict['hits']) - hits = fields_dict['hit'] - result_list = [] - search_results_count = len(hits) - print "FOUND {} RESULTS".format(search_results_count) - for item in hits: - result = item['fields'] - if len(result_list) < result_to_send_count: - result_list.append(result) - else: - break - return result_list - - def check_message(self, msg): - # check the message and if query wasn't understood, post error - if 'could not find' in msg: - self.print_error(msg) - - def print_error(self, message): - """ - print error messages in the terminal - if slack webhook is set up, post the errors to slack - """ - print("[{0}] - ".format(datetime.now().strftime( - "%Y-%m-%d %H:%M:%S")) + message) - response = None - if SLACK["url"]: - response = requests.post( - SLACK["url"], - data=json.dumps({ - "attachments": [{ - "author_name": "HealthTools API", - "color": "warning", - "pretext": "[SMS] Could not find a result for this SMS.", - "fields": [{ - "title": "Message", - "value": message, - "short": False - }] - }] - }), - headers={"Content-Type": "application/json"}) - return response diff --git a/healthtools/core.py b/healthtools/core.py index 557e94f..0f9e1a5 100644 --- a/healthtools/core.py +++ b/healthtools/core.py @@ -1,6 +1,56 @@ -from flask import Flask +from werkzeug.local import LocalProxy +from flask import Flask, current_app +from elasticsearch import Elasticsearch, RequestsHttpConnection +from requests_aws4auth import AWS4Auth +from healthtools import settings -def create_app(): + +def create_app(config={}): app = Flask('healthtools') + app.config.from_object(settings) + app.config.update(config) return app + + +def get_es(): + app = current_app._get_current_object() + if not hasattr(app, '_es_instance'): + if 'aws' in app.config.get('ELASTICSEARCH_HOST'): + host = app.config.get('ELASTICSEARCH_HOST') + awsauth = AWS4AuthNotUnicode(app.config.get('AWS_ACCESS_KEY'), + app.config.get('AWS_SECRET_KEY'), + app.config.get('AWS_REGION'), + 'es') + app._es_instance = Elasticsearch( + hosts=[{'host': host, 'port': 443}], + http_auth=awsauth, + use_ssl=True, + verify_certs=True, + connection_class=RequestsHttpConnection, + timeout=120 + ) + else: + app._es_instance = Elasticsearch( + app.config.get('ELASTICSEARCH_HOST'), + timeout=120 + ) + return app._es_instance + + +def get_es_index(): + app = current_app._get_current_object() + return app.config.get('ELASTICSEARCH_INDEX') + + +es = LocalProxy(get_es) +es_index = LocalProxy(get_es_index) + + +# Work-around: https://github.com/sam-washington/requests-aws4auth/issues/24 + +class AWS4AuthNotUnicode(AWS4Auth): + def __call__(self, req): + req = super(AWS4AuthNotUnicode, self).__call__(req) + req.headers = {str(name): value for name, value in req.headers.items()} + return req diff --git a/healthtools/elastic_search.py b/healthtools/elastic_search.py deleted file mode 100644 index dca29e9..0000000 --- a/healthtools/elastic_search.py +++ /dev/null @@ -1,138 +0,0 @@ -from elasticsearch import Elasticsearch, RequestsHttpConnection -from requests_aws4auth import AWS4Auth - -from healthtools_ke_api.settings import AWS, ES -from healthtools_ke_api.views.serializer import JSONSerializerPython2 -import re - - -class Elastic(object): - """ - Common class for elastic search client and methods - """ - - def __init__(self): - # client host for aws elastic search service - if "aws" in ES["host"]: - # set up authentication credentials - awsauth = AWS4Auth(AWS["access_key"], - AWS["secret_key"], AWS["region"], 'es') - self.es_client = Elasticsearch( - hosts=ES["host"], - port=int(ES["port"]), - http_auth=awsauth, - use_ssl=True, - verify_certs=True, - connection_class=RequestsHttpConnection, - serializer=JSONSerializerPython2() - ) - else: - self.es_client = Elasticsearch( - "{}:{}".format(ES["host"], ES["port"])) - - @staticmethod - def remove_keyword(query): - """ - Remove keyword from search term - """ - query_formatted = query.strip().lower() - keywords = ['dr', 'dr.', 'doctor', 'nurse', - 'co', 'c.o.', 'c.o', 'clinical officer'] - for word in keywords: - regex = r'(? 0) - - def test_cos_endpoint_with_bad_query(self): - response = self.client.get("/clinical-officers/search.json?q=") - self.assertIn("A query is required.", response.data) - - def test_cos_endpoint_gets_doctors(self): - response = self.client.get("/clinical-officers/search.json?q=Ann") - self.assertIn("success", response.data) diff --git a/healthtools/tests/doctors_api.py b/healthtools/tests/doctors_api.py deleted file mode 100644 index ddd489a..0000000 --- a/healthtools/tests/doctors_api.py +++ /dev/null @@ -1,21 +0,0 @@ -from unittest import TestCase -from healthtools_ke_api import app -from healthtools_ke_api.elastic_search import Elastic - - -class TestDoctorsAPI(TestCase): - def setUp(self): - self.client = app.test_client() - self.es = Elastic() - - def test_gets_doctors_from_elasticsearch(self): - doctors = self.es.get_from_elasticsearch("doctors", "BHATT") - self.assertTrue(len(doctors) > 0) - - def test_doctors_endpoint_with_bad_query(self): - response = self.client.get("/doctors/search.json?q=") - self.assertIn("A query is required.", response.data) - - def test_doctors_endpoint_gets_doctors(self): - response = self.client.get("/doctors/search.json?q=Marie") - self.assertIn("success", response.data) diff --git a/healthtools/tests/nhif_inpatient.py b/healthtools/tests/nhif_inpatient.py deleted file mode 100644 index b657d63..0000000 --- a/healthtools/tests/nhif_inpatient.py +++ /dev/null @@ -1,19 +0,0 @@ -from unittest import TestCase -from healthtools_ke_api import app -from healthtools_ke_api.views.search import Elastic - -class TestNhifInpatientAPI(TestCase): - def setUp(self): - self.client = app.test_client() - self.es = Elastic() - def test_gets_nhifin_from_elasticsearch(self): - nhif_inpatient = self.es.get_from_elasticsearch("nhif-inpatient", "ABRAR HEALTH SERVICES LTD") - self.assertTrue(len(nhif_inpatient) > 0) - - def test_nhifin_endpoint_with_bad_query(self): - response = self.client.get("/nhif-inpatient/search.json?q=") - self.assertIn("A query is required.", response.data) - - def test_nhifin_endpoint_gets_nhif_inpatient(self): - response = self.client.get("/nhif-inpatient/search.json?q=Kenyatta Hsopital") - self.assertIn("success", response.data) \ No newline at end of file diff --git a/healthtools/tests/nhif_outpatient.py b/healthtools/tests/nhif_outpatient.py deleted file mode 100644 index a1c6fa1..0000000 --- a/healthtools/tests/nhif_outpatient.py +++ /dev/null @@ -1,19 +0,0 @@ -from unittest import TestCase -from healthtools_ke_api import app -from healthtools_ke_api.views.search import Elastic - -class TestNhifOutpatientAPI(TestCase): - def setUp(self): - self.client = app.test_client() - self.es = Elastic() - def test_gets_nhifop_from_elasticsearch(self): - nhif_outpatient = self.es.get_from_elasticsearch("nhif-outpatient", "Jacob") - self.assertTrue(len(nhif_outpatient) > 0) - - def test_nhifop_endpoint_with_bad_query(self): - response = self.client.get("/nhif-outpatient/search.json?q=") - self.assertIn("A query is required.", response.data) - - def test_nhifop_endpoint_gets_nhif_outpatient(self): - response = self.client.get("/nhif-outpatient/search.json?q=Kenyatta") - self.assertIn("success", response.data) \ No newline at end of file diff --git a/healthtools/tests/nhif_outpatient_cs.py b/healthtools/tests/nhif_outpatient_cs.py deleted file mode 100644 index 01c9086..0000000 --- a/healthtools/tests/nhif_outpatient_cs.py +++ /dev/null @@ -1,20 +0,0 @@ -from unittest import TestCase -from healthtools_ke_api import app -from healthtools_ke_api.views.search import Elastic - - -class TestNhifOutpatientCSAPI(TestCase): - def setUp(self): - self.client = app.test_client() - self.es = Elastic() - def test_gets_nhifopcs_from_elasticsearch(self): - nhif_outpatient_cs = self.es.get_from_elasticsearch("nhif-outpatient-cs", "Kenyatta hospital") - self.assertTrue(len(nhif_outpatient_cs) > 0) - - def test_nhifopcs_endpoint_with_bad_query(self): - response = self.client.get("/nhif-outpatient-cs/search.json?q=") - self.assertIn("A query is required.", response.data) - - def test_nhifopcs_endpoint_gets_nhif_outpatient_cs(self): - response = self.client.get("/nhif-outpatient-cs/search.json?q=Kenyatta") - self.assertIn("success", response.data) \ No newline at end of file diff --git a/healthtools/tests/nurses_api.py b/healthtools/tests/nurses_api.py deleted file mode 100644 index 968f984..0000000 --- a/healthtools/tests/nurses_api.py +++ /dev/null @@ -1,31 +0,0 @@ -from unittest import TestCase -from healthtools_ke_api import app -from healthtools_ke_api.views.nurses import get_nurses_from_nc_registry - - -class TestNursesAPI(TestCase): - def setUp(self): - self.client = app.test_client() - - def test_gets_nurses_from_nc_registry(self): - nurses = get_nurses_from_nc_registry("Marie") - self.assertTrue(len(nurses) > 0) - - def test_gets_nurses_from_nc_registry_handle_inexistent_nurse(self): - nurses = get_nurses_from_nc_registry("ihoafiho39023u8") - self.assertEqual(len(nurses), 0) - - def test_nurses_endpoint_handles_bad_query(self): - response = self.client.get("/nurses/search.json?q=") - self.assertIn("A query is required.", response.data) - - def test_nurses_endpoint_gets_nurses(self): - response = self.client.get("/nurses/search.json?q=Marie") - self.assertIn("success", response.data) - - def test_nurses_endpoint_can_retrieve_cached_result(self): - # call once - self.client.get("/nurses/search.json?q=Marie") - # second time should retrieve cached result - response = self.client.get("/nurses/search.json?q=Marie") - self.assertIn("X-Retrieved-From-Cache", response.headers.keys()) diff --git a/healthtools/tests/sms_api.py b/healthtools/tests/sms_api.py deleted file mode 100644 index 6070557..0000000 --- a/healthtools/tests/sms_api.py +++ /dev/null @@ -1,34 +0,0 @@ -from unittest import TestCase -from healthtools_ke_api import app -from healthtools_ke_api.views.sms_handler import send_sms -from healthtools_ke_api.build_query import BuildQuery - -bq = BuildQuery() - - -class TestSmsApi(TestCase): - def setUp(self): - self.client = app.test_client() - - def test_sms_requires_message_and_number(self): - response = self.client.get("/sms") - message = "The url parameters 'message' and 'phoneNumber' are required." - self.assertEqual(message, response.data) - - def test_query_response(self): - response = bq.build_query_response('nurse mary') - self.assertTrue(len(response[0]) > 0) - - def test_send_sms(self): - response = self.client.get( - "/sms", query_string={"phoneNumber": "+254726075080", - "message": 'nurse mary'}) - self.assertEqual(200, response.status_code) - - def test_queries_not_understood_post_to_slack(self): - message = bq.build_query_response('Test SMS error posts to slack') - response = self.client.get( - "/sms", query_string={"phoneNumber": "+254726075080", - "message": message[0]}) - self.assertEqual( - 'Could not find a doctor with that name', response.data) diff --git a/healthtools/tests/sms_api_clinical_officers.py b/healthtools/tests/sms_api_clinical_officers.py deleted file mode 100644 index f0db58b..0000000 --- a/healthtools/tests/sms_api_clinical_officers.py +++ /dev/null @@ -1,26 +0,0 @@ -from unittest import TestCase -from healthtools_ke_api import app -from healthtools_ke_api.views.sms_handler import build_query_response, send_sms - - -class TestSmsApi(TestCase): - def setUp(self): - self.client = app.test_client() - - def test_sms_requires_message_and_number(self): - response = self.client.get("/sms") - message = "The url parameters 'message' and 'phoneNumber' are required." - self.assertEqual(message, response.data) - - def test_query_response(self): - response = build_query_response('clinical officer John') - self.assertTrue(len(response[0]) > 0) - - def test_send_sms(self): - response = self.client.get("/sms", query_string={"phoneNumber": "09", "message": 'CO KEBAGENDI'}) - self.assertEqual(200, response.status_code) - - def test_queries_not_understood_post_to_slack(self): - message = build_query_response('Test SMS error posts to slack') - response = self.client.get("/sms", query_string={"phoneNumber": "09", "message": message[0]}) - self.assertEqual('Could not find a Clinical Officer with that name', response.data) \ No newline at end of file diff --git a/healthtools/tests/sms_api_doctors.py b/healthtools/tests/sms_api_doctors.py deleted file mode 100644 index 51f3e7d..0000000 --- a/healthtools/tests/sms_api_doctors.py +++ /dev/null @@ -1,26 +0,0 @@ -from unittest import TestCase -from healthtools_ke_api import app -from healthtools_ke_api.views.sms_handler import build_query_response, send_sms - - -class TestSmsApi(TestCase): - def setUp(self): - self.client = app.test_client() - - def test_sms_requires_message_and_number(self): - response = self.client.get("/sms") - message = "The url parameters 'message' and 'phoneNumber' are required." - self.assertEqual(message, response.data) - - def test_query_response(self): - response = build_query_response('daktari John') - self.assertTrue(len(response[0]) > 0) - - def test_send_sms(self): - response = self.client.get("/sms", query_string={"phoneNumber": "09", "message": 'oncologist KEBAGENDI'}) - self.assertEqual(200, response.status_code) - - def test_queries_not_understood_post_to_slack(self): - message = build_query_response('Test SMS error posts to slack') - response = self.client.get("/sms", query_string={"phoneNumber": "09", "message": message[0]}) - self.assertEqual('Could not find a Doctor with that name', response.data) \ No newline at end of file diff --git a/healthtools/tests/sms_api_healthfacilities.py b/healthtools/tests/sms_api_healthfacilities.py deleted file mode 100644 index 03e2cfc..0000000 --- a/healthtools/tests/sms_api_healthfacilities.py +++ /dev/null @@ -1,27 +0,0 @@ -from unittest import TestCase -from healthtools_ke_api import app -from healthtools_ke_api.views.sms_handler import build_query_response, send_sms - - - -class TestSmsApi(TestCase): - def setUp(self): - self.client = app.test_client() - - def test_sms_requires_message_and_number(self): - response = self.client.get("/sms") - message = "The url parameters 'message' and 'phoneNumber' are required." - self.assertEqual(message, response.data) - - def test_query_response(self): - response = build_query_response('sanatorium Kituni') - self.assertTrue(len(response[0]) > 0) - - def test_send_sms(self): - response = self.client.get("/sms", query_string={"phoneNumber": "09", "message": 'hf Emayian'}) - self.assertEqual(200, response.status_code) - - def test_queries_not_understood_post_to_slack(self): - message = build_query_response('Test SMS error posts to slack') - response = self.client.get("/sms", query_string={"phoneNumber": "09", "message": message[0]}) - self.assertEqual('Could not find a health facility with that name', response.data) \ No newline at end of file diff --git a/healthtools/tests/sms_api_nhif_accredited_hospitals.py b/healthtools/tests/sms_api_nhif_accredited_hospitals.py deleted file mode 100644 index b0cf0d0..0000000 --- a/healthtools/tests/sms_api_nhif_accredited_hospitals.py +++ /dev/null @@ -1,26 +0,0 @@ -from unittest import TestCase -from healthtools_ke_api import app -from healthtools_ke_api.views.sms_handler import build_query_response, send_sms - - -class TestSmsApi(TestCase): - def setUp(self): - self.client = app.test_client() - - def test_sms_requires_message_and_number(self): - response = self.client.get("/sms") - message = "The url parameters 'message' and 'phoneNumber' are required." - self.assertEqual(message, response.data) - - def test_query_response(self): - response = build_query_response('nhif EDELVALE') - self.assertTrue(len(response[0]) > 0) - - def test_send_sms(self): - response = self.client.get("/sms", query_string={"phoneNumber": "09", "message": 'hospital fund alif'}) - self.assertEqual(200, response.status_code) - - def test_queries_not_understood_post_to_slack(self): - message = build_query_response('Test SMS error posts to slack') - response = self.client.get("/sms", query_string={"phoneNumber": "09", "message": message[0]}) - self.assertEqual('Could not find a NHIF accredited hospital with that name', response.data) diff --git a/healthtools/tests/sms_api_nurse.py b/healthtools/tests/sms_api_nurse.py deleted file mode 100644 index e874292..0000000 --- a/healthtools/tests/sms_api_nurse.py +++ /dev/null @@ -1,26 +0,0 @@ -from unittest import TestCase -from healthtools_ke_api import app -from healthtools_ke_api.views.sms_handler import build_query_response, send_sms - - -class TestSmsApi(TestCase): - def setUp(self): - self.client = app.test_client() - - def test_sms_requires_message_and_number(self): - response = self.client.get("/sms") - message = "The url parameters 'message' and 'phoneNumber' are required." - self.assertEqual(message, response.data) - - def test_query_response(self): - response = build_query_response('nurse mary') - self.assertTrue(len(response[0]) > 0) - - def test_send_sms(self): - response = self.client.get("/sms", query_string={"phoneNumber": "09", "message": 'nurse mary'}) - self.assertEqual(200, response.status_code) - - def test_queries_not_understood_post_to_slack(self): - message = build_query_response('Test SMS error posts to slack') - response = self.client.get("/sms", query_string={"phoneNumber": "09", "message": message[0]}) - self.assertEqual('Could not find a doctor with that name', response.data) diff --git a/healthtools/tests/test_telegram_bot.py b/healthtools/tests/test_telegram_bot.py deleted file mode 100644 index e3c3ce4..0000000 --- a/healthtools/tests/test_telegram_bot.py +++ /dev/null @@ -1,25 +0,0 @@ -import re -import unittest - -from healthtools_ke_api import app -from healthtools_ke_api.views.telegram_bot import telegram_bot as tg - -from healthtools_ke_api.settings import DEBUG, TGBOT - -DEBUG = False - - -class TestTelegramBot(unittest.TestCase): - def setUp(self): - self.client = app.test_client() - - def test_https_webhook(self): - """ - Test a https url is provided - """ - url = re.search("https", TGBOT["BOT_WEBHOOK_URL"]) - self.assertTrue(url) - - def test_invalid_get(self): - response = self.client.get('/' + TGBOT["BOT_TOKEN"]) - self.assertEqual(response.status_code, 405) diff --git a/healthtools/views/base_api.py b/healthtools/views/base_api.py index bd012bc..da9473a 100644 --- a/healthtools/views/base_api.py +++ b/healthtools/views/base_api.py @@ -1,4 +1,4 @@ -from flask import Blueprint, request, jsonify +from flask import Blueprint, redirect blueprint = Blueprint('base_api', __name__) @@ -6,4 +6,5 @@ @blueprint.route('/') def index(): - return jsonify({'results': [], 'status': 'OK'}) + # TODO: Redirect to HealthTools docs instead + return redirect('https://github.com/CodeForAfricaLabs/HealthTools.API') diff --git a/healthtools/views/clinical_officers.py b/healthtools/views/clinical_officers.py deleted file mode 100644 index f84163f..0000000 --- a/healthtools/views/clinical_officers.py +++ /dev/null @@ -1,64 +0,0 @@ -from flask import Blueprint, request, jsonify, current_app - -from healthtools_ke_api.elastic_search import Elastic -from healthtools_ke_api.analytics import track_event - -clinical_officers_api = Blueprint('clinical_officers_api', __name__) - - -@clinical_officers_api.route('/', methods=['GET']) -def index(): - ''' - Landing endpoint - ''' - msg = { - "name": "API to Kenyan Clinical Officers registry", - "authentication": [], - "endpoints": { - "/": { - "methods": ["GET"] - }, - "/clinical-officers/search.json": { - "methods": ["GET"], - "args": { - "q": {"required": True} - } - }, - } - } - return jsonify(msg) - - -@clinical_officers_api.route('/search.json', methods=['GET']) -def search(): - try: - query = request.args.get('q') - if not query or len(query) < 1: - return jsonify({ - "error": "A query is required.", - "results": "", - "data": {"clinical_officers": []} - }) - - # get clinical_officers by that name from aws - response = {} - es = Elastic() - clinical_officers = es.get_from_elasticsearch('clinical-officers', query) - - if not clinical_officers: - response["message"] = "No clinical officer by that name found." - - track_event(current_app.config.get('GA_TRACKING_ID'), - 'Clinical-Officers', 'search', request.remote_addr, - label=query, value=len(clinical_officers)) - response["data"] = {"clinical_officers": clinical_officers} - response["status"] = "success" - - results = jsonify(response) - return results - except Exception as err: - return jsonify({ - "status": "error", - "message": str(err), - "data": {"clinical_officers": []} - }) diff --git a/healthtools/views/doctors.py b/healthtools/views/doctors.py deleted file mode 100644 index 5ba8153..0000000 --- a/healthtools/views/doctors.py +++ /dev/null @@ -1,61 +0,0 @@ -from flask import Blueprint, request, jsonify, current_app - -from healthtools_ke_api.analytics import track_event -from healthtools_ke_api.elastic_search import Elastic - -doctors_api = Blueprint('doctors_api', __name__) - - -@doctors_api.route('/', methods=['GET']) -def index(): - ''' - Landing endpoint - ''' - msg = { - "name": "API to the Kenyan doctors registry", - "authentication": [], - "endpoints": { - "/": {"methods": ["GET"]}, - "/doctors/search.json": { - "methods": ["GET"], - "args": { - "q": {"required": True} - } - }, - } - } - return jsonify(msg) - - -@doctors_api.route('/search.json', methods=['GET']) -def search(): - try: - query = request.args.get('q') - if not query or len(query) < 1: - return jsonify({ - "error": "A query is required.", - "results": "", - "data": {"doctors": []} - }) - - # get doctors by that name from aws - response = {} - es = Elastic() - doctors = es.get_from_elasticsearch('doctors', query) - - if not doctors: - response["message"] = "No doctor by that name found." - - track_event(current_app.config.get('GA_TRACKING_ID'), 'Doctor', 'search', - request.remote_addr, label=query, value=len(doctors)) - response["data"] = {"doctors": doctors} - response["status"] = "success" - - results = jsonify(response) - return results - except Exception as err: - return jsonify({ - "status": "error", - "message": str(err), - "data": {"doctors": []} - }) diff --git a/healthtools/views/json_serializer.py b/healthtools/views/json_serializer.py deleted file mode 100644 index a4196d6..0000000 --- a/healthtools/views/json_serializer.py +++ /dev/null @@ -1,18 +0,0 @@ -import json -from elasticsearch import serializer, compat, exceptions - - -class JSONSerializerPython2(serializer.JSONSerializer): - """Override elasticsearch library serializer to ensure it encodes utf characters during json dump. - See original at: https://github.com/elastic/elasticsearch-py/blob/master/elasticsearch/serializer.py#L42 - A description of how ensure_ascii encodes unicode characters to ensure they can be sent across the wire - as ascii can be found here: https://docs.python.org/2/library/json.html#basic-usage - """ - def dumps(self, data): - # don't serialize strings - if isinstance(data, compat.string_types): - return data - try: - return json.dumps(data, default=self.default, ensure_ascii=True) - except (ValueError, TypeError) as e: - raise exceptions.SerializationError(data, e) diff --git a/healthtools/views/nhif_inpatient.py b/healthtools/views/nhif_inpatient.py deleted file mode 100644 index 13079ce..0000000 --- a/healthtools/views/nhif_inpatient.py +++ /dev/null @@ -1,63 +0,0 @@ -from flask import Blueprint, request, jsonify, make_response, json, current_app -from bs4 import BeautifulSoup -from elastic_search import Elastic - -from healthtools_ke_api.analytics import track_event - -import requests - -nhif_inpatient_api = Blueprint('nhif_inpatient_api', __name__) - -@nhif_inpatient_api.route('/', methods=['GET']) -def index(): - ''' - Landing endpoint - ''' - msg = { - "name": "API to the NHIF inpatient registry", - "authentication": [], - "endpoints": { - "/": {"methods": ["GET"]}, - "/nhif-inpatient/search.json": { - "methods": ["GET"], - "args": { - "q": {"required": True} - } - }, - } - } - return jsonify(msg) - -@nhif_inpatient_api.route('/search.json', methods=['GET']) -def search(): - try: - query = request.args.get('q') - if not query or len(query) < 1: - return jsonify({ - "error": "A query is required.", - "results": "", - "data": {"nhif_inpatient": []} - }) - - response = {} - es = Elastic() - nhif_inpatient = es.get_from_elasticsearch('nhif-inpatient', query) - - if not nhif_inpatient: - response["message"] = "No NHIF inpatient facility by that name found." - - track_event(current_app.config.get('GA_TRACKING_ID'), 'Nhif-Inpatient', 'search', - request.remote_addr, label=query, value=len(nhif_inpatient)) - - response["data"] = {"nhif_inpatient": nhif_inpatient} - response["status"] = "success" - - results = jsonify(response) - return results - - except Exception as err: - return jsonify({ - "status": "error", - "message": str(err), - "data": {"nhif_inpatient": []} - }) \ No newline at end of file diff --git a/healthtools/views/nhif_outpatient.py b/healthtools/views/nhif_outpatient.py deleted file mode 100644 index b178434..0000000 --- a/healthtools/views/nhif_outpatient.py +++ /dev/null @@ -1,63 +0,0 @@ -from flask import Blueprint, request, jsonify, make_response, json, current_app -from bs4 import BeautifulSoup -from elastic_search import Elastic - -from healthtools_ke_api.analytics import track_event - -import requests - -nhif_outpatient_api = Blueprint('nhif_outpatient_api', __name__) - -@nhif_outpatient_api.route('/', methods=['GET']) -def index(): - ''' - Landing endpoint - ''' - msg = { - "name": "API to the NHIF inpatient registry", - "authentication": [], - "endpoints": { - "/": {"methods": ["GET"]}, - "/nhif-outpatient/search.json": { - "methods": ["GET"], - "args": { - "q": {"required": True} - } - }, - } - } - return jsonify(msg) - -@nhif_outpatient_api.route('/search.json', methods=['GET']) -def search(): - try: - query = request.args.get('q') - if not query or len(query) < 1: - return jsonify({ - "error": "A query is required.", - "results": "", - "data": {"nhif_outpatient": []} - }) - - response = {} - es = Elastic() - nhif_outpatient = es.get_from_elasticsearch('nhif-outpatient', query) - - if not nhif_outpatient: - response["message"] = "No NHIF Outpatient facility by that name found." - - track_event(current_app.config.get('GA_TRACKING_ID'), 'Nhif-Outpatient', 'search', - request.remote_addr, label=query, value=len(nhif_outpatient)) - - response["data"] = {"nhif_outpatient": nhif_outpatient} - response["status"] = "success" - - results = jsonify(response) - return results - - except Exception as err: - return jsonify({ - "status": "error", - "message": str(err), - "data": {"nhif_outpatient": []} - }) \ No newline at end of file diff --git a/healthtools/views/nhif_outpatient_cs.py b/healthtools/views/nhif_outpatient_cs.py deleted file mode 100644 index 063199b..0000000 --- a/healthtools/views/nhif_outpatient_cs.py +++ /dev/null @@ -1,63 +0,0 @@ -from flask import Blueprint, request, jsonify, make_response, json, current_app -from bs4 import BeautifulSoup -from elastic_search import Elastic - -from healthtools_ke_api.analytics import track_event - -import requests - -nhif_outpatient_cs_api = Blueprint('nhif_outpatient_cs_api', __name__) - -@nhif_outpatient_cs_api.route('/', methods=['GET']) -def index(): - ''' - Landing endpoint - ''' - msg = { - "name": "API to the NHIF Outpatient CS registry", - "authentication": [], - "endpoints": { - "/": {"methods": ["GET"]}, - "/nhif-outpatientcs/search.json": { - "methods": ["GET"], - "args": { - "q": {"required": True} - } - }, - } - } - return jsonify(msg) - -@nhif_outpatient_cs_api.route('/search.json', methods=['GET']) -def search(): - try: - query = request.args.get('q') - if not query or len(query) < 1: - return jsonify({ - "error": "A query is required.", - "results": "", - "data": {"nhif-outpatient-cs": []} - }) - - response = {} - es = Elastic() - nhif_outpatientcs = es.get_from_elasticsearch('nhif-outpatient-cs', query) - - if not nhif_outpatientcs: - response["message"] = "No NHIF Outpatient CS facility by that name found." - - track_event(current_app.config.get('GA_TRACKING_ID'), 'Nhif-OutpatientCS', 'search', - request.remote_addr, label=query, value=len(nhif_outpatientcs)) - - response["data"] = {"nhif_outpatientcs": nhif_outpatientcs} - response["status"] = "success" - - results = jsonify(response) - return results - - except Exception as err: - return jsonify({ - "status": "error", - "message": str(err), - "data": {"nhif_outpatientcs": []} - }) \ No newline at end of file diff --git a/healthtools/views/nurses.py b/healthtools/views/nurses.py deleted file mode 100644 index acc8062..0000000 --- a/healthtools/views/nurses.py +++ /dev/null @@ -1,112 +0,0 @@ -from bs4 import BeautifulSoup -from flask import Blueprint, request, jsonify, make_response, json, current_app - -from healthtools_ke_api.analytics import track_event -from healthtools_ke_api.settings import MEMCACHED_URL - -import requests -import memcache - -nurses_api = Blueprint('nurses_api', __name__) -cache = memcache.Client([MEMCACHED_URL], debug=True) # cache server - -nurse_fields = ["name", "licence_no", "valid_till"] -NURSING_COUNCIL_URL = "http://nckenya.com/services/search.php?p=1&s={}" - - -@nurses_api.route('/', methods=['GET']) -def index(): - ''' - Landing endpoint - ''' - msg = { - "name": "Nursing Council of Kenya API", - "authentication": [], - "endpoints": { - "/": {"methods": ["GET"]}, - "/nurses/search.json": { - "methods": ["GET"], - "args": { - "q": {"required": True} - } - }, - } - } - return jsonify(msg) - - -@nurses_api.route('/search.json', methods=['GET']) -def search(): - try: - query = request.args.get('q') - if not query or len(query) < 1: - return jsonify({ - "error": "A query is required.", - "results": "", - "data": {"nurses": []} - }) - - # try to get queried result first - cached_result = cache.get(query.replace(" ", "")) - if cached_result: - num_cached_results = len(json.loads( - cached_result.data)["data"]["nurses"]) - track_event(current_app.config.get('GA_TRACKING_ID'), 'Nurse', 'search', - request.remote_addr, label=query, value=num_cached_results) - response = make_response(cached_result) - response.headers["X-Retrieved-From-Cache"] = True - return response - - # get nurses by that name from nursing council site - response = {} - nurses = get_nurses_from_nc_registry(query) - if not nurses: - response["message"] = "No nurse by that name found." - - # send action to google analytics - track_event(current_app.config.get('GA_TRACKING_ID'), - 'Nurse', 'search', - request.remote_addr, label=query, value=len(nurses)) - - response["data"] = {"nurses": nurses} - response["status"] = "success" - - results = jsonify(response) - cache.set(query.replace(" ", ""), results, - time=345600) # expire after 4 days - return results - - except Exception as err: - return jsonify({ - "status": "error", - "message": str(err), - "data": {"nurses": []} - }) - - -def get_nurses_from_nc_registry(query): - ''' - Get nurses from the nursing council of Kenya registry - ''' - url = NURSING_COUNCIL_URL.format(query) - response = requests.get(url) - nurses = [] - - if "No results" in response.content: - return nurses - - # make soup for parsing out of response and get the table - soup = BeautifulSoup(response.content, "html.parser") - table = soup.find("table", {"class": "zebra"}).find("tbody") - rows = table.find_all("tr") - - # parse table for the nurses data - for row in rows: - # only the columns we want - columns = row.find_all("td")[:len(nurse_fields)] - columns = [text.text.strip() for text in columns] - - entry = dict(zip(nurse_fields, columns)) - nurses.append(entry) - - return nurses diff --git a/healthtools/views/search_api.py b/healthtools/views/search_api.py index 3de9a50..5263469 100644 --- a/healthtools/views/search_api.py +++ b/healthtools/views/search_api.py @@ -6,18 +6,20 @@ blueprint = Blueprint('search_api', __name__) -@blueprint.route('/search', methods=['GET']) -@blueprint.route('/search/', methods=['GET']) +@blueprint.route('/search', methods=['GET'], strict_slashes=False) +@blueprint.route('/search/', methods=['GET'], strict_slashes=False) def index(doc_type=None): query = request.args.get('q') - results = run_query(query, doc_type) + result = run_query(query, doc_type) # TODO: Return run_query message here # Error with run_query (run_query returns false) - if(not results): + if(not result): return jsonify({ - 'results': [], + 'result': {'hits': [], 'total': 0}, 'status': 'FAILED', - 'msg': '' # TODO: Pass run_query msg here. + 'msg': '' # TODO: Pass run_query message here. }) - return jsonify({'results': results, 'status': 'OK'}) + # TODO: Log event here (send to Google Analytics) + + return jsonify({'result': result, 'status': 'OK'}) diff --git a/healthtools/views/sms_handler.py b/healthtools/views/sms_handler.py deleted file mode 100644 index 3a64b99..0000000 --- a/healthtools/views/sms_handler.py +++ /dev/null @@ -1,67 +0,0 @@ -import getpass -import json -import re -import requests - -from datetime import datetime -from flask import Blueprint, request, current_app - -from healthtools_ke_api.analytics import track_event -from healthtools_ke_api.settings import SLACK - -from healthtools_ke_api.views.nurses import get_nurses_from_nc_registry -from healthtools_ke_api.elastic_search import Elastic -from healthtools_ke_api.build_query import BuildQuery - - -SMS_SEND_URL = 'http://ke.mtechcomm.com/remote' -SMS_RESULT_COUNT = 4 # Number of results to be send via sms -DOC_KEYWORDS = ['doc', 'daktari', 'doctor', 'oncologist', 'dr'] -CO_KEYWORDS = ['CO', 'clinical officer', - 'clinic officer', 'clinical', 'clinical oficer', ] -NO_KEYWORDS = ['nurse', 'no', 'nursing officer', - 'mhuguzi', 'muuguzi', 'RN', 'Registered Nurse'] -NHIF_KEYWORDS = ['nhif', 'bima', 'insurance', - 'insurance fund', 'health insurance', 'hospital fund'] -HF_KEYWORDS = ['hf', 'hospital', 'dispensary', 'clinic', - 'hospitali', 'sanatorium', 'health centre'] -es = Elastic() -bq = BuildQuery() - -sms_handler = Blueprint("sms_handler", __name__) - - -@sms_handler.route("/sms", methods=['GET']) -def sms(): - name = request.args.get("message") - phone_number = request.args.get("phoneNumber") - if not name or not phone_number: - return "The url parameters 'message' and 'phoneNumber' are required." - # Track Event SMS RECEIVED - track_event(current_app.config.get('GA_TRACKING_ID'), 'smsquery', 'receive', - encode_cid(phone_number), label='lambda', value=2) - msg = bq.build_query_response(name) - resp = send_sms(phone_number, msg[0]) - # Track Event SMS SENT - track_event(current_app.config.get('GA_TRACKING_ID'), 'smsquery', 'send', - encode_cid(phone_number), label='lambda', value=2) - - return msg[0] - - -def send_sms(phone_number, msg): - params = { - 'user': current_app.config.get('SMS_USER'), - 'pass': current_app.config.get('SMS_PASS'), - 'messageID': 0, - 'shortCode': current_app.config.get('SMS_SHORTCODE'), - 'MSISDN': phone_number, - 'MESSAGE': msg - } - resp = requests.get(SMS_SEND_URL, params=params) - return resp - - -def encode_cid(phone_number): - # TODO: Generate a hash instead of using phone number - return phone_number diff --git a/healthtools/views/telegram_bot/__init__.py b/healthtools/views/telegram_bot/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/healthtools/views/telegram_bot/telegram_bot.py b/healthtools/views/telegram_bot/telegram_bot.py deleted file mode 100644 index 56f3578..0000000 --- a/healthtools/views/telegram_bot/telegram_bot.py +++ /dev/null @@ -1,35 +0,0 @@ -import json -import requests - -from flask import Blueprint, request, jsonify -from telegram import Update - -from healthtools_ke_api.views.telegram_bot import telegram_manager as manager - - -telegram_bot = Blueprint('telegram_bot', __name__) - -# Initialize the Telegram Bot handler -manager.setup() - - -@telegram_bot.route('/' + manager.TOKEN, methods=['POST']) -def webhook(): - if request.method == "POST": - if not manager.DEBUG: - # retrieve the message in JSON and then transform it to Telegram - # object - update = Update.de_json(request.get_json(force=True), manager.bot) - - # Start conversation - manager.update_queue.put(update) - - return jsonify({ - "Success": "Telegram Bot is up and running on Webhooks", - "status": 200}) - else: - return jsonify({ - "Success": "Telegram Bot is up and running on polling", - "status": 200}) - else: - return jsonify({"Error": "Method not allowed", "status": 405}) diff --git a/healthtools/views/telegram_bot/telegram_manager.py b/healthtools/views/telegram_bot/telegram_manager.py deleted file mode 100644 index 2c88513..0000000 --- a/healthtools/views/telegram_bot/telegram_manager.py +++ /dev/null @@ -1,299 +0,0 @@ -import json -import logging -import os -import re -import requests -import string -import time - -from flask import Flask, Blueprint, request, jsonify - -from queue import Queue -from threading import Thread - -from emoji import emojize -from telegram import (Bot, Update, ReplyKeyboardMarkup, ParseMode, - ReplyKeyboardRemove) -from telegram.ext import (Dispatcher, Updater, CommandHandler, - ConversationHandler, Filters, MessageHandler, RegexHandler) - -from healthtools_ke_api.build_query import BuildQuery -from healthtools_ke_api.settings import DEBUG, TGBOT - -TOKEN = TGBOT["BOT_TOKEN"] -SERVER_IP = TGBOT["SERVER_IP"] -PORT = TGBOT["TELEGRAM_PORT"] -CERT_FILE = TGBOT["CERT_FILE"] -KEY_FILE = TGBOT["KEY_FILE"] - -WEBHOOK_URL = TGBOT["BOT_WEBHOOK_URL"] -WEBHOOK_URL = WEBHOOK_URL + "/" + TOKEN - -CONTEXT = (CERT_FILE, KEY_FILE) - -# States -CHOOSING, TYPING_REPLY = range(2) - -bot = Bot(TOKEN) -build_query = BuildQuery() -build_query.SMS_RESULT_COUNT = 5 - -# Enable logging -if DEBUG: - logging.basicConfig( - level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') -else: - logging.basicConfig( - level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') - - -logger = logging.getLogger("telegram.bot.Manager") - - -if DEBUG: - updater = Updater(bot=bot) - dp = updater.dispatcher -else: - update_queue = Queue() - dp = Dispatcher(bot, update_queue) - - -# Custom keyboard with reply options -reply_keyboard = [["Clinical Officer", "Doctor", "Nurse"], - ["NHIF Accredited Hospital: Inpatient", - "NHIF Accredited Hospital: Outpatient"], - ["Health Facility"]] - -markup = ReplyKeyboardMarkup( - reply_keyboard, one_time_keyboard=True) - - -def start_polling(): - """ - Activates schedulers of all setups, and starts polling updates from Telegram. - """ - - logger.info('Start polling') - - # getUpdate does not work if webhook is set. So we delete any webhook - updater.bot.set_webhook() - - updater.start_polling(poll_interval=1.0, timeout=20) - updater.idle() - - -def set_webhook(webhook_url, cert, key): - """ - Activates schedulers of all setups, setups webhook, and - starts small http server to listen for updates via this webhook. - """ - logger.info('Start webhook') - - # Delete any webhook to avoid Conflict: terminated by other setWebhook - bot.setWebhook() - time.sleep(5) # to avoid error 4RetryAfter: Flood control exceeded - - # Using nginx + bot - bot.setWebhook(url=webhook_url - # certificate=open(CERT_FILE, 'rb') - ) - - print ("\nWebhook set: %s \n", bot.getWebhookInfo().url) - - thread = Thread(target=dp.start, name='dp') - thread.start() - - return (update_queue, dp) - - -def facts_to_str(user_data): - facts = list() - - for key, value in user_data.items(): - facts.append("%s - %s" % (key, value)) - - return "\n".join(facts).join(["\n", "\n"]) - - -def start(bot, update): - - chat_id = update.message.chat_id - user = update.message.from_user.first_name - - wave = emojize(':wave:', use_aliases=True) - welcome_msg = ( - u"*Hello* {0} {1} \n" - "My name is `Healthtools Bot`.\n\n" - "*Here's what I can do for you:*\n" - "1. Check to see if your doctor, nurse, or clinical officer is registered\n" - "2. Find out which facilities your NHIF card will cover in your county\n" - "3. Find the nearest doctor or health facility\n" - "\nSend /cancel to stop talking to me.\n".format(user, wave) - ).encode('utf-8') - - bot.send_message( - chat_id=chat_id, - text=welcome_msg, - parse_mode=ParseMode.MARKDOWN, - reply_markup=markup, - ) - - # Call the next function - return CHOOSING - -# TODO: Handle edited_message in these too - - -def regular_choice(bot, update, user_data): - """ - Show summary of user choice and input - """ - chat_id = update.message.chat_id - - text = update.message.text - user_data['choice'] = text - - msg = "Please enter the name of a `%s` you want to find" % text.title() - - bot.send_message( - chat_id=chat_id, - text=msg, - parse_mode=ParseMode.MARKDOWN, - ) - - # Call next function: received_information - # return TYPING_REPLY - return TYPING_REPLY - - -def received_information(bot, update, user_data): - """ - Show summary of user choice and input - """ - chat_id = update.message.chat_id - - text = update.message.text - choice = user_data['choice'] - user_data[choice] = text - del user_data['choice'] - - update.message.reply_text("Here's what you have asked me to search" - "%s" - % facts_to_str(user_data)) - - bot.sendChatAction(chat_id=chat_id, action="typing") - - # Now we fetch the data - results = fetch_data(user_data) - - print("\n") - print(results) - - # Remove multiple whitespaces - results = re.sub(" +", " ", str(results[0])) - search_msg = ( - "*Search Results*\n" - "%s" % results - ) - - bot.send_message( - chat_id=chat_id, - text=search_msg, - parse_mode=ParseMode.MARKDOWN, - reply_markup=markup, - ) - - # Empty the user_data dictionary - user_data.clear() - - # Call functi0n regular_choice - # return CHOOSING - return CHOOSING - - -def fetch_data(user_data): - """ - Fetch the data requested by the user - """ - query = user_data.keys()[-1] - - if query: - keyword = str(query).encode("utf-8") - search_term = user_data[keyword] - - if re.search("NHIF Accredited Hospital", keyword): - keyword = re.sub("accredited hospital: ", "", - keyword, flags=re.IGNORECASE) - - if keyword == "Health Facility": - keyword = "hf" - - keyword = keyword.lower() - query = keyword + " " + search_term - - else: - return "Keyword not valid" - - search_results = build_query.build_query_response(query) - return search_results - - -def cancel(bot, update): - user = update.message.from_user - update.message.reply_text("Catch you later %s! Hope to talk to you soon." % user.first_name, - reply_markup=ReplyKeyboardRemove()) - - return ConversationHandler.END - - -def unknown(bot, update): - bot.sendMessage( - chat_id=update.message.chat_id, - text="Sorry, I didn't understand that command.") - - -def error(bot, update, error): - logger.warning("Update % s caused error % s" % (update, error)) - - -def setup(): - dp.add_handler(CommandHandler('help', start)) - - # Add conversation handler with the states - dp.add_handler(ConversationHandler( - # Handler object to trigger the start of the conversation - entry_points=[CommandHandler('start', start)], - - # Conversation states - states={ - CHOOSING: [RegexHandler('^(Clinical Officer|Doctor|Nurse|Health Facility|NHIF Accredited Hospital)', - regular_choice, - pass_user_data=True, - edited_updates=True), - ], - - TYPING_REPLY: [MessageHandler(Filters.text, - received_information, - pass_user_data=True, - edited_updates=True), - ], - - }, - - fallbacks=[CommandHandler('cancel', cancel)], - - # Allow user to restart a conversation with an entry point - allow_reentry=True - )) - - dp.add_handler(MessageHandler(Filters.command, unknown)) - dp.add_error_handler(error) - - if DEBUG: - # Use webhook - return start_polling() - else: - # Start Webhook - set_webhook(webhook_url=WEBHOOK_URL, - cert=CERT_FILE, - key=KEY_FILE) diff --git a/setup.py b/setup.py index b979596..a001d02 100644 --- a/setup.py +++ b/setup.py @@ -13,6 +13,8 @@ install_requires=[ 'flask', 'requests', - 'bs4' + 'bs4', + 'elasticsearch', + 'requests_aws4auth' ], )