Browse Source

Merge remote-tracking branch 'origin/dev'

Steve Nyemba 4 years ago
parent
commit
f25a5831c0

+ 49 - 9
README.md

@@ -22,16 +22,26 @@ We wrote this frame to be used in both command line or as a library within in yo
     pip install --upgrade git+https://hiplab.mc.vanderbilt.edu/git/lab/parse-edi.git
     pip install --upgrade git+https://hiplab.mc.vanderbilt.edu/git/lab/parse-edi.git
 
 
 ## Usage 
 ## Usage 
-
+ 
 **cli :**
 **cli :**
 
 
 1. signup to get parsing configuration
 1. signup to get parsing configuration
 
 
+    The parser is driven by a configuration file that specifies fields to parse and how to parse them. You need by signing up, to get a copy of the configuration file. 
+
         healthcare-io.py --signup <email> [--store <mongo|sqlite>]
         healthcare-io.py --signup <email> [--store <mongo|sqlite>]
-        
-2. parsing claims in a folder
 
 
-        healthcare-io.py --parse <claims|remits> --folder <path> [--batch <n>] [--resume]
+2. check version       
+
+    Occasionally the attributes in the configuration file may change, This function will determine if there is a new version available.
+
+        healthcare-io.py --check-update
+
+3. parsing data in a folder
+
+    The parser will recursively traverse a directory with claims and or remittances
+
+        healthcare-io.py --parse --folder <path> [--batch <n>] [--resume]
         
         
         with :
         with :
             --parse     tells the engine what to parse claims or remits
             --parse     tells the engine what to parse claims or remits
@@ -39,12 +49,42 @@ We wrote this frame to be used in both command line or as a library within in yo
             --batch     number of processes to spawn to parse the files
             --batch     number of processes to spawn to parse the files
             --resume    tells the parser to resume parsing 
             --resume    tells the parser to resume parsing 
                         if all files weren't processed or new files were added into the folder
                         if all files weren't processed or new files were added into the folder
-3. dashboard
-    
-    There is a built-in dashboard that has displays descriptive analytics in a web browser
-    
-        healthcare-io.py --server <port> [--context <name>]    
+
+4. export data to a relational data-store
+
+    The parser will export data into other data-stores as a  relational tables allowing users to construct views to support a variety of studies.
+
+        healthcare-io.py --export <835|837> --config <path-export.json>
+
+        with:
+            --config    configuration to support data-store
         
         
+
+    The configuration file needed to implement export is modelled after the following template:
+
+
+        {
+            "provider":"<postgresql|redshift|mysql|mariadb>",
+            "db":"mydatabase",
+                [
+                    "host":"server-name","port":5432,
+                    "user":"me","password":"!@#z4qm",
+                    "schema":"target-schema"
+                ]
+            }
+
+    **parameters:**
+
+        provider    postgresql,redshift,mysql or mariadb (supported providers)
+        db          name of the database
+    
+    **optional:**
+        schema      name of the target schema. If not provided we will assume the default
+        host        host of the database. If not provided assuming localhost
+        port        port value of the database if not provided the default will be used
+        user        database user name. If not provided we assume security settings to trust
+        password    password of database user. If not set we assume security settings to trust
+
 **Embedded in Code   :**
 **Embedded in Code   :**
 
 
 The Healthcare/IO **parser** can be used within your code base as a library and handle storing data in a data store of choice
 The Healthcare/IO **parser** can be used within your code base as a library and handle storing data in a data store of choice

+ 20 - 3
healthcareio/Dockerfile

@@ -1,4 +1,8 @@
-FROM ubuntu:bionic-20200403
+#
+# Let us create an image for healthcareio
+#   The image will contain the {X12} Parser and the 
+# FROM ubuntu:bionic-20200403
+FROM ubuntu:focal
 RUN ["apt","update","--fix-missing"]
 RUN ["apt","update","--fix-missing"]
 RUN ["apt-get","upgrade","-y"]
 RUN ["apt-get","upgrade","-y"]
 
 
@@ -6,9 +10,22 @@ RUN ["apt-get","-y","install","apt-utils"]
 
 
 RUN ["apt","update","--fix-missing"]
 RUN ["apt","update","--fix-missing"]
 RUN ["apt-get","upgrade","-y"]
 RUN ["apt-get","upgrade","-y"]
-RUN ["apt-get","install","-y","sqlite3","sqlite3-pcre","libsqlite3-dev","python3-dev","python3","python3-pip","git","python3-virtualenv"]
+RUN ["apt-get","install","-y","mongo","sqlite3","sqlite3-pcre","libsqlite3-dev","python3-dev","python3","python3-pip","git","python3-virtualenv","wget"]
 #
 #
 #
 #
+RUN ["pip3","install","--upgrade","pip"]
+# RUN ["pip3","install","git+https://healthcare.the-phi.com/git/code/parser.git","botocore"]
 USER health-user
 USER health-user
+#
+# This volume is where the data will be loaded from (otherwise it is assumed the user will have it in the container somehow)
+#
+VOLUME ["/data"]
+#
+# This is the port from which some degree of monitoring can/will happen
+EXPOSE 80
+# wget https://healthcareio.the-phi.com/git/code/parser.git/bootup.sh 
+COPY bootup.sh bootup.sh
+ENTRYPOINT ["bash","-C"]
+CMD ["bootup.sh"]
 # VOLUME ["/home/health-user/healthcare-io/","/home-healthuser/.healthcareio"]
 # VOLUME ["/home/health-user/healthcare-io/","/home-healthuser/.healthcareio"]
-# RUN ["pip3","install","git+https://healthcareio.the-phi.com/git"]
+# RUN ["pip3","install","git+https://healthcareio.the-phi.com/git"]

+ 4 - 0
healthcareio/__init__.py

@@ -16,5 +16,9 @@ Usage :
 """
 """
 
 
 from healthcareio import analytics
 from healthcareio import analytics
+from healthcareio import server
+from healthcareio import export
 import healthcareio.x12 as x12
 import healthcareio.x12 as x12
+import healthcareio.params as params
+
 # from healthcareio import server
 # from healthcareio import server

+ 113 - 21
healthcareio/analytics.py

@@ -11,7 +11,7 @@ import transport
 import matplotlib.pyplot as plt
 import matplotlib.pyplot as plt
 import re, base64
 import re, base64
 # from weasyprint import HTML, CSS
 # from weasyprint import HTML, CSS
-COLORS = ["#f79256","#7dcfb6","#fbd1a2","#00b2ca","#1d4e89","#4682B4","#c5c3c6","#4c5c68","#1985a1","#f72585","#7209b7","#3a0ca3","#4361ee","#4cc9f0","#ff595e","#ffca3a","#8ac926","#1982c4","#6a4c93"]
+COLORS = ["#fbd1a2","#00b2ca","#1d4e89","#4682B4","#c5c3c6","#4c5c68","#1985a1","#f72585","#7209b7","#3a0ca3","#4361ee","#4cc9f0","#ff595e","#ffca3a","#8ac926","#1982c4","#6a4c93"]
 class stdev :
 class stdev :
     def __init__(self) :
     def __init__(self) :
         self.values = []
         self.values = []
@@ -149,11 +149,16 @@ class Apex :
     This class will format a data-frame to work with Apex charting engine
     This class will format a data-frame to work with Apex charting engine
     """
     """
     @staticmethod
     @staticmethod
-    def apply(item):
+    def apply(item,theme={'mode':'light','palette':'palette6'}):
         pointer = item['chart']['type']
         pointer = item['chart']['type']
         if hasattr(Apex,pointer) :
         if hasattr(Apex,pointer) :
             pointer = getattr(Apex,pointer)
             pointer = getattr(Apex,pointer)
+            
             options = pointer(item)
             options = pointer(item)
+            if 'apex' in options and 'colors' in options['apex'] :
+                del options['apex']['colors']
+            if 'apex' in options :    
+                options['apex']['theme'] = theme
             options['responsive']= [
             options['responsive']= [
                 {
                 {
                 'breakpoint': 1,
                 'breakpoint': 1,
@@ -168,6 +173,18 @@ class Apex :
             print ("Oops")
             print ("Oops")
         pass
         pass
     @staticmethod
     @staticmethod
+    def radial(item):
+        df = item['data']
+        x = item['chart']['axis']['x']
+        y  = item['chart']['axis']['y']
+        
+        labels = df[y].tolist()
+        values = [float(np.round(value,2)) for value in df[x].tolist()]
+        chart = {"type":"radialBar","height":200}
+        option = {"chart":chart,"series":values,"labels":labels,"plotOptions":{"radialBar":{"hollow":{"size":"70%"}}}}
+        return {'apex':option}
+
+    @staticmethod
     def scatter(item):
     def scatter(item):
         options = Apex.spline(item)
         options = Apex.spline(item)
         options['apex']['chart']['type'] = 'scatter'
         options['apex']['chart']['type'] = 'scatter'
@@ -175,7 +192,7 @@ class Apex :
     @staticmethod
     @staticmethod
     def scalar(item):
     def scalar(item):
         _df = item['data']
         _df = item['data']
-        print (_df)
+        
         name = _df.columns.tolist()[0]
         name = _df.columns.tolist()[0]
         value = _df[name].values.round(2)[0]
         value = _df[name].values.round(2)[0]
         html = '<div class="scalar"><div class="value">:value</div><div class="label">:label</div></div>'
         html = '<div class="scalar"><div class="value">:value</div><div class="label">:label</div></div>'
@@ -235,16 +252,17 @@ class Apex :
         @TODO: alias this with bar (!= column)
         @TODO: alias this with bar (!= column)
         """
         """
         df = item['data']
         df = item['data']
+        
         N = df.shape[0] if df.shape[0] < 10 else 10
         N = df.shape[0] if df.shape[0] < 10 else 10
         axis = item['chart']['axis']
         axis = item['chart']['axis']
         y = axis['y']
         y = axis['y']
         if type(y) == list :
         if type(y) == list :
             y = y[0]
             y = y[0]
         axis['x'] = [axis['x']] if type(axis['x']) != list else axis['x']
         axis['x'] = [axis['x']] if type(axis['x']) != list else axis['x']
-        if not set(axis['x']) & set(df.columns.tolist()) :
-            print (set(axis['x']) & set(df.columns.tolist()))
-            print (axis['x'])
-            print (df.columns)
+        # if not set(axis['x']) & set(df.columns.tolist()) :
+            # print (set(axis['x']) & set(df.columns.tolist()))
+            # print (axis['x'])
+            # print (df.columns)
             # df.columns = axis['x']
             # df.columns = axis['x']
         series = []
         series = []
         _min=_max = 0
         _min=_max = 0
@@ -294,7 +312,6 @@ class Apex :
         values are x-axis
         values are x-axis
         """
         """
         df = item['data']
         df = item['data']
-        
         if df.shape [0]> 1 :
         if df.shape [0]> 1 :
             y_cols,x_cols = item['chart']['axis']['y'],item['chart']['axis']['x']
             y_cols,x_cols = item['chart']['axis']['y'],item['chart']['axis']['x']
             labels = df[y_cols].values.tolist()
             labels = df[y_cols].values.tolist()
@@ -302,10 +319,11 @@ class Apex :
             values = df[x_cols].values.round(2).tolist()
             values = df[x_cols].values.round(2).tolist()
         else:
         else:
             labels = [name.upper().replace('_',' ') for name in df.columns.tolist()]
             labels = [name.upper().replace('_',' ') for name in df.columns.tolist()]
+            df = df.astype(float)
             values = df.values.round(2).tolist()[0] if df.shape[1] > 1 else df.values.round(2).tolist()
             values = df.values.round(2).tolist()[0] if df.shape[1] > 1 else df.values.round(2).tolist()
             
             
         colors  = COLORS[:len(values)]
         colors  = COLORS[:len(values)]
-        options = {"series":values,"colors":colors,"labels":labels,"chart":{"type":"donut"},"plotOptions":{"pie":{"customScale":.8}},"legend":{"position":"right"}}
+        options = {"series":values,"colors":colors,"labels":labels,"dataLabels":{"enabled":True,"style":{"colors":["#000000"]},"dropShadow":{"enabled":False}},"chart":{"type":"donut","width":200},"plotOptions":{"pie":{"customScale":.9}},"legend":{"position":"right"}}
         return {"apex":options}
         return {"apex":options}
 
 
         pass
         pass
@@ -329,43 +347,117 @@ class engine :
             _args['type'] = 'mongo.MongoReader'
             _args['type'] = 'mongo.MongoReader'
         else:
         else:
             _args['type'] = 'disk.SQLiteReader'
             _args['type'] = 'disk.SQLiteReader'
-        self.reader = transport.factory.instance(**_args)
+        self.store_config = _args ;
+    
+    def filter (self,**args):
+        """
+            type: claims or remits
+            filter  optional identifier claims, procedures, taxonomy, ...
+        """
+        
+        
+        _m = {'claim':'837','claims':'837','remits':'835','remit':'835'}
+        table = _m[ args['type']]
+        _analytics = self.info[table]
+        if 'index' in args :
+            index = int(args['index'])
+            _analytics = [_analytics[index]]
+            
+        _info = list(_analytics) #if 'filter' not in args else [item for item in analytics if args['filter'] == item['id']]
+        # conn = lite.connect(self.store_config['args']['path'],isolation_level=None)
+        # conn.create_aggregate("stdev",1,stdev)
+        DB_TYPE = 'mongo' if (type(self.reader) == transport.mongo.MongoReader) else 'sql'
+        if DB_TYPE == 'mongo' :
+            self.store_config['args']['doc'] = args['type']
+        
+        self.reader = transport.factory.instance(**self.store_config)
+        r = []
+        for row in _info :
+            pipeline = row['pipeline']
+            
+            index = 0
+            for item in pipeline:
+                if not item[DB_TYPE] :
+                    continue
+                query = {DB_TYPE:item[DB_TYPE]}
+                
+                df = pd.DataFrame(self.reader.read(**query)) #item)
+                df = df.fillna('N/A')
+                # item['data'] = df
+                chart = item['chart']
+                pipe = {"data":df,"chart":chart}
+                for key in list(item.keys()) :
+                    if key not in ["chart","data","mongo","sql","couch"] :
+                        pipe[key] = item[key]
+                
+                
+                
 
 
+                r.append(pipe)
+        self.reader.close()
+        return {"id":_info[0]['id'],'pipeline':r}
+    
     def apply (self,**args) :
     def apply (self,**args) :
         """
         """
             type: claims or remits
             type: claims or remits
             filter  optional identifier claims, procedures, taxonomy, ...
             filter  optional identifier claims, procedures, taxonomy, ...
         """
         """
+        
+        
         _m = {'claim':'837','claims':'837','remits':'835','remit':'835'}
         _m = {'claim':'837','claims':'837','remits':'835','remit':'835'}
         # key = '837' if args['type'] == 'claims' else '835'
         # key = '837' if args['type'] == 'claims' else '835'
         table = _m[ args['type']]
         table = _m[ args['type']]
-        analytics = self.info[table]
+        
+        _analytics = self.info[table]
         if 'index' in args :
         if 'index' in args :
             index = int(args['index'])
             index = int(args['index'])
-            analytics = [analytics[index]]
+            _analytics = [_analytics[index]]
             
             
-        _info = list(analytics) if 'filter' not in args else [item for item in analytics if args['filter'] == item['id']]
+        _info = list(_analytics) if 'filter' not in args else [item for item in analytics if args['filter'] == item['id']]
         # conn = lite.connect(self.store_config['args']['path'],isolation_level=None)
         # conn = lite.connect(self.store_config['args']['path'],isolation_level=None)
         # conn.create_aggregate("stdev",1,stdev)
         # conn.create_aggregate("stdev",1,stdev)
-        DB_TYPE = 'mongo' if (type(self.reader) == transport.mongo.MongoReader) else 'sql'
+        #
+        # @TODO: Find a better way to handle database variance
+        #
+        # DB_TYPE = 'mongo' if (type(self.reader) == transport.mongo.MongoReader) else 'sql'
+        
+        if 'mongo' in self.store_config['type'] :
+            DB_TYPE='mongo'
+        else:
+            DB_TYPE='sql'
+            self.store_config['args']['table'] = args['type']
+        
+        self.reader = transport.factory.instance(**self.store_config)
         r = []
         r = []
         for row in _info :
         for row in _info :
-            
-            for item in row['pipeline'] :
+            pipeline = row['pipeline']
+            index = 0
+            for item in pipeline:
                 # item['data'] = pd.read_sql(item['sql'],conn)
                 # item['data'] = pd.read_sql(item['sql'],conn)
-                query = {DB_TYPE:item[DB_TYPE]}
-                item['data'] = self.reader.read(**item)
+                # query = {DB_TYPE:item[DB_TYPE]}
+                query = item[DB_TYPE]
+                if not query :
+                    continue
+                if DB_TYPE == 'sql' :
+                    query = {"sql":query}
+                
+                item['data'] = self.reader.read(**query) #item)
                 if 'serialize' in args :
                 if 'serialize' in args :
                     
                     
-                    item['data'] = json.dumps(item['data'].to_dict(orient='record')) if type(item['data']) == pd.DataFrame else item['data']
+                    # item['data'] = json.dumps(item['data'].to_dict(orient='record')) if type(item['data']) == pd.DataFrame else item['data']
+                    item['data'] = json.dumps(item['data'].to_dict('record')) if type(item['data']) == pd.DataFrame else item['data']
                 else:
                 else:
                     item['data'] = (pd.DataFrame(item['data']))
                     item['data'] = (pd.DataFrame(item['data']))
-                    
+                pipeline[index] = item
+                index += 1
+            #
+            #
+            row['pipeline']= pipeline
                     
                     
                 # if 'info' in item:
                 # if 'info' in item:
                 #     item['info'] = item['info'].replace(":rows",str(item["data"].shape[0]))
                 #     item['info'] = item['info'].replace(":rows",str(item["data"].shape[0]))
         # conn.close()
         # conn.close()
-        
+        self.reader.close()
         return _info
         return _info
 
 
     def _html(self,item) :
     def _html(self,item) :

+ 37 - 0
healthcareio/docker/Dockerfile

@@ -0,0 +1,37 @@
+#
+# Let us create an image for healthcareio
+#   The image will contain the {X12} Parser and the 
+# FROM ubuntu:bionic-20200403
+FROM ubuntu:focal
+RUN ["apt-get","update","--fix-missing"]
+RUN ["apt-get","upgrade","-y"]
+
+RUN ["apt-get","-y","install","apt-utils"]
+
+RUN ["apt-get","update","--fix-missing"]
+RUN ["apt-get","upgrade","-y"]
+RUN ["apt-get","install","-y","mongodb","sqlite3","sqlite3-pcre","libsqlite3-dev","python3-dev","python3","python3-pip","git","python3-virtualenv","wget"]
+#
+#
+RUN ["pip3","install","--upgrade","pip"]
+RUN ["pip3","install","numpy","pandas","git+https://dev.the-phi.com/git/steve/data-transport","botocore","matplotlib"]
+# RUN ["pip3","install","git+https://healthcare.the-phi.com/git/code/parser.git","botocore"]
+# RUN ["useradd", "-ms", "/bin/bash", "health-user"]
+# USER health-user
+#
+# This volume is where the data will be loaded from (otherwise it is assumed the user will have it in the container somehow)
+#
+VOLUME ["/data","/app/healthcareio"]
+WORKDIR /app
+ENV PYTHONPATH="/app"
+
+#
+# This is the port from which some degree of monitoring can/will happen
+EXPOSE 80
+EXPOSE 27017    
+# wget https://healthcareio.the-phi.com/git/code/parser.git/bootup.sh 
+COPY bootup.sh bootup.sh
+ENTRYPOINT ["bash","-C"]
+CMD ["bootup.sh"]
+# VOLUME ["/home/health-user/healthcare-io/","/home-healthuser/.healthcareio"]
+# RUN ["pip3","install","git+https://healthcareio.the-phi.com/git"]

+ 10 - 0
healthcareio/docker/bootup.sh

@@ -0,0 +1,10 @@
+set -e
+/etc/init.d/mongodb start
+cd /app
+export
+export PYTHONPATH=$PWD
+ls
+# python3 healthcareio/healthcare-io.py --signup $EMAIL --store mongo 
+# python3 healthcareio/healthcare-io.py --analytics --port 80 --debug 
+
+bash

BIN
healthcareio/docs/835-relational-structure.jpeg


BIN
healthcareio/docs/837-relational-structure.jpeg


File diff suppressed because it is too large
+ 314 - 0
healthcareio/docs/claims-837-relational-structure.xmi


File diff suppressed because it is too large
+ 331 - 0
healthcareio/docs/remits-835-relational-structure.xmi


+ 176 - 210
healthcareio/healthcare-io.py

@@ -32,9 +32,10 @@ Usage :
 from healthcareio.params import SYS_ARGS
 from healthcareio.params import SYS_ARGS
 from transport import factory
 from transport import factory
 import requests
 import requests
-
 from healthcareio import analytics
 from healthcareio import analytics
 from healthcareio import server
 from healthcareio import server
+
+
 from healthcareio.parser import get_content
 from healthcareio.parser import get_content
 import os
 import os
 import json
 import json
@@ -43,6 +44,10 @@ import numpy as np
 from multiprocessing import Process
 from multiprocessing import Process
 import time
 import time
 from healthcareio import x12
 from healthcareio import x12
+from healthcareio.export import export
+import smart
+from healthcareio.server import proxy
+import pandas as pd
 
 
 PATH = os.sep.join([os.environ['HOME'],'.healthcareio'])
 PATH = os.sep.join([os.environ['HOME'],'.healthcareio'])
 OUTPUT_FOLDER = os.sep.join([os.environ['HOME'],'healthcare-io'])
 OUTPUT_FOLDER = os.sep.join([os.environ['HOME'],'healthcare-io'])
@@ -53,10 +58,28 @@ if not os.path.exists(PATH) :
 import platform
 import platform
 import sqlite3 as lite
 import sqlite3 as lite
 # PATH = os.sep.join([os.environ['HOME'],'.edi-parser'])
 # PATH = os.sep.join([os.environ['HOME'],'.edi-parser'])
-def register (**args) :
+HELP_MESSAGE = """
+        cli:
+        
+            healthcare-io.py    --<[signup|init]> <email> --store <sqlite|mongo> [--batch <value>]
+            healthcare-io.py    --parse --folder <path> [--batch <value>] [--resume]
+            healthcare-io.py    --check-update
+            healthcare-io.py    --export <835|837> --config <config-path>
+        action :
+            --signup|init   signup user and get configuration file
+            --parse         starts parsing
+            --check         checks for updates
+            --export        export data of a 835 or 837 into another database
+        parameters :
+            --<[signup|init]>   signup or get a configuration file from a parsing server
+            --folder            location of the files (the program will recursively traverse it)
+            --store             data store mongo or sqlite or mongodb
+            --resume            will attempt to resume if there was an interruption
+        """
+def signup (**args) :
     """
     """
     :email  user's email address
     :email  user's email address
-    :url    url of the provider to register
+    :url    url of the provider to signup
     """
     """
     
     
     email = args['email']
     email = args['email']
@@ -121,77 +144,77 @@ def init():
 #
 #
 # Global variables that load the configuration files
 # Global variables that load the configuration files
 
 
-def parse(**args):
-    """
-    This function will parse the content of a claim or remittance (x12 format) give the following parameters
-    :filename   absolute path of the file to be parsed
-    :type       claims|remits in x12 format
-    """
-    global INFO
-    if not INFO :
-        INFO = init()
-    if args['type'] == 'claims' :
-        CONFIG = INFO['parser']['837']
-    elif args['type'] == 'remits' :
-        CONFIG = INFO['parser']['835']
-    else:
-        CONFIG = None
-    if CONFIG :
-        # CONFIG = CONFIG[-1] if 'version' not in args and (args['version'] < len(CONFIG)) else CONFIG[0]
-        CONFIG = CONFIG[int(args['version'])-1] if 'version' in SYS_ARGS and int(SYS_ARGS['version']) < len(CONFIG) else CONFIG[-1]
-        SECTION = CONFIG['SECTION']
-        os.environ['HEALTHCAREIO_SALT'] = INFO['owner']
+# def parse(**args):
+#     """
+#     This function will parse the content of a claim or remittance (x12 format) give the following parameters
+#     :filename   absolute path of the file to be parsed
+#     :type       claims|remits in x12 format
+#     """
+#     global INFO
+#     if not INFO :
+#         INFO = init()
+#     if args['type'] == 'claims' :
+#         CONFIG = INFO['parser']['837']
+#     elif args['type'] == 'remits' :
+#         CONFIG = INFO['parser']['835']
+#     else:
+#         CONFIG = None
+#     if CONFIG :
+#         # CONFIG = CONFIG[-1] if 'version' not in args and (args['version'] < len(CONFIG)) else CONFIG[0]
+#         CONFIG = CONFIG[int(args['version'])-1] if 'version' in SYS_ARGS and int(SYS_ARGS['version']) < len(CONFIG) else CONFIG[-1]
+#         SECTION = CONFIG['SECTION']
+#         os.environ['HEALTHCAREIO_SALT'] = INFO['owner']
 
 
         
         
-        return get_content(args['filename'],CONFIG,SECTION)
-def resume (files,id,config):
-    _args = config['store'].copy()
-    if 'mongo' in config['store']['type'] :
-        _args['type'] = 'mongo.MongoReader'
-        reader = factory.instance(**_args)
-    _files = []
-    if 'resume' in config['analytics'] :
-        _args = config['analytics']['resume'][id]
-        _files = reader.read(**_args)
-        _files = [item['name'] for item in _files if item['name'] != None]
-        return list(set(files) - set(_files))
+#         return get_content(args['filename'],CONFIG,SECTION)
+# def resume (files,id,config):
+#     _args = config['store'].copy()
+#     if 'mongo' in config['store']['type'] :
+#         _args['type'] = 'mongo.MongoReader'
+#         reader = factory.instance(**_args)
+#     _files = []
+#     if 'resume' in config['analytics'] :
+#         _args = config['analytics']['resume'][id]
+#         _files = reader.read(**_args)
+#         _files = [item['name'] for item in _files if item['name'] != None]
+#         return list(set(files) - set(_files))
         
         
-    return files
-    pass
-def apply(files,store_info,logger_info=None):
-    """
-        :files          list of files to be processed in this given thread/process
-        :store_info     information about data-store, for now disk isn't thread safe
-        :logger_info    information about where to store the logs
-    """
+    # return files
+    # pass
+# def apply(files,store_info,logger_info=None):
+#     """
+#         :files          list of files to be processed in this given thread/process
+#         :store_info     information about data-store, for now disk isn't thread safe
+#         :logger_info    information about where to store the logs
+#     """
 
 
-    if not logger_info :
-        logger = factory.instance(type='disk.DiskWriter',args={'path':os.sep.join([info['out-folder'],SYS_ARGS['parse']+'.log'])})
-    else:
-        logger = factory.instance(**logger_info)
+#     if not logger_info :
+#         logger = factory.instance(type='disk.DiskWriter',args={'path':os.sep.join([info['out-folder'],SYS_ARGS['parse']+'.log'])})
+#     else:
+#         logger = factory.instance(**logger_info)
 
 
-    writer = factory.instance(**store_info)
-    for filename in files :
+#     writer = factory.instance(**store_info)
+#     for filename in files :
         
         
-        if filename.strip() == '':
-            continue
-        # content,logs = get_content(filename,CONFIG,CONFIG['SECTION'])  
-        # 
-        try:              
-            content,logs = parse(filename = filename,type=SYS_ARGS['parse'])
+#         if filename.strip() == '':
+#             continue
+#         # content,logs = get_content(filename,CONFIG,CONFIG['SECTION'])  
+#         # 
+#         try:              
+#             content,logs = parse(filename = filename,type=SYS_ARGS['parse'])
             
             
-            if content :                        
-                writer.write(content)
-            if logs :
-                [logger.write(dict(_row,**{"parse":SYS_ARGS['parse']})) for _row in logs]
-            else:
-                logger.write({"parse":SYS_ARGS['parse'],"name":filename,"completed":True,"rows":len(content)})
-        except Exception as e:
+#             if content :                        
+#                 writer.write(content)
+#             if logs :
+#                 [logger.write(dict(_row,**{"parse":SYS_ARGS['parse']})) for _row in logs]
+#             else:
+#                 logger.write({"parse":SYS_ARGS['parse'],"name":filename,"completed":True,"rows":len(content)})
+#         except Exception as e:
             
             
-            logger.write({"parse":SYS_ARGS['parse'],"filename":filename,"completed":False,"rows":-1,"msg":e.args[0]})
-        # print ([filename,len(content)])
-        #
-        # @TODO: forward this data to the writer and log engine 
+#             logger.write({"parse":SYS_ARGS['parse'],"filename":filename,"completed":False,"rows":-1,"msg":e.args[0]})
+#         # print ([filename,len(content)])
+#         #
+#         # @TODO: forward this data to the writer and log engine 
         #
         #
 def upgrade(**args):
 def upgrade(**args):
     """
     """
@@ -200,13 +223,28 @@ def upgrade(**args):
     """    
     """    
     url = args['url'] if 'url' in args else URL+"/upgrade"
     url = args['url'] if 'url' in args else URL+"/upgrade"
     headers = {"key":args['key'],"email":args["email"],"url":url}
     headers = {"key":args['key'],"email":args["email"],"url":url}
+def check(**_args):
+    """
+    This function will check if there is an update available (versions are in the configuration file)
+    :param url
+    """
+    url = _args['url'][:-1] if _args['url'].endswith('/') else _args['url']
+    url = url + "/version"
+    if 'version' not in _args :
+        version = {"_id":"version","current":0.0}
+    else:
+        version = _args['version']
+    http = requests.session()    
+    r = http.get(url)
+    return r.json()
     
     
 if __name__ == '__main__' :
 if __name__ == '__main__' :
     info = init()
     info = init()
     
     
     if 'out-folder' in SYS_ARGS :
     if 'out-folder' in SYS_ARGS :
         OUTPUT_FOLDER = SYS_ARGS['out-folder']
         OUTPUT_FOLDER = SYS_ARGS['out-folder']
-        
+    SYS_ARGS['url'] = SYS_ARGS['url'] if 'url' in SYS_ARGS else URL
+    
     if set(list(SYS_ARGS.keys())) & set(['signup','init']):
     if set(list(SYS_ARGS.keys())) & set(['signup','init']):
         #
         #
         # This command will essentially get a new copy of the configurations
         # This command will essentially get a new copy of the configurations
@@ -214,10 +252,10 @@ if __name__ == '__main__' :
         #
         #
         
         
         email = SYS_ARGS['signup'].strip() if 'signup' in SYS_ARGS else SYS_ARGS['init']
         email = SYS_ARGS['signup'].strip() if 'signup' in SYS_ARGS else SYS_ARGS['init']
-        url = SYS_ARGS['url'] if 'url' in SYS_ARGS else 'https://healthcareio.the-phi.com'
+        url = SYS_ARGS['url'] if 'url' in SYS_ARGS else URL
         store = SYS_ARGS['store'] if 'store' in SYS_ARGS else 'sqlite'
         store = SYS_ARGS['store'] if 'store' in SYS_ARGS else 'sqlite'
         db='healthcareio' if 'db' not in SYS_ARGS else SYS_ARGS['db']
         db='healthcareio' if 'db' not in SYS_ARGS else SYS_ARGS['db']
-        register(email=email,url=url,store=store,db=db)
+        signup(email=email,url=url,store=store,db=db)
     # else:
     # else:
     #     m = """
     #     m = """
     #     usage:
     #     usage:
@@ -241,51 +279,31 @@ if __name__ == '__main__' :
         if 'file' in SYS_ARGS :
         if 'file' in SYS_ARGS :
             files = [SYS_ARGS['file']]  if not os.path.isdir(SYS_ARGS['file']) else []
             files = [SYS_ARGS['file']]  if not os.path.isdir(SYS_ARGS['file']) else []
         if 'folder' in SYS_ARGS and os.path.exists(SYS_ARGS['folder']):
         if 'folder' in SYS_ARGS and os.path.exists(SYS_ARGS['folder']):
-            names = os.listdir(SYS_ARGS['folder'])
-            files   += [os.sep.join([SYS_ARGS['folder'],name]) for name in names if not os.path.isdir(os.sep.join([SYS_ARGS['folder'],name]))]
+            for root,_dir,f in os.walk(SYS_ARGS['folder']) :
+                
+                if f :
+                    files += [os.sep.join([root,name]) for name in f]
+            
+            # names = os.listdir(SYS_ARGS['folder'])
+            # files   += [os.sep.join([SYS_ARGS['folder'],name]) for name in names if not os.path.isdir(os.sep.join([SYS_ARGS['folder'],name]))]
         else:
         else:
             #
             #
-            # raise an erro
+            # raise an error
+            
             pass
             pass
         #
         #
         # if the user has specified to resume, we should look into the logs and pull the files processed and those that haven't
         # if the user has specified to resume, we should look into the logs and pull the files processed and those that haven't
         #
         #
         if 'resume' in SYS_ARGS :
         if 'resume' in SYS_ARGS :
-            files = resume(files,SYS_ARGS['parse'],info)
-            print (["Found ",len(files)," files unprocessed"])
+            store_config = json.loads( (open(os.sep.join([PATH,'config.json']))).read() )
+            files = proxy.get.resume(files,store_config )
+            # print (["Found ",len(files)," files unprocessed"])
         #
         #
         # @TODO: Log this here so we know what is being processed or not
         # @TODO: Log this here so we know what is being processed or not
         SCOPE = None
         SCOPE = None
         
         
         if files : #and ('claims' in SYS_ARGS['parse'] or 'remits' in SYS_ARGS['parse']):
         if files : #and ('claims' in SYS_ARGS['parse'] or 'remits' in SYS_ARGS['parse']):
-            # logger = factory.instance(type='disk.DiskWriter',args={'path':os.sep.join([info['out-folder'],SYS_ARGS['parse']+'.log'])})
-            # if info['store']['type'] == 'disk.DiskWriter' :
-            #     info['store']['args']['path'] += (os.sep + 'healthcare-io.json')
-            # elif info['store']['type'] == 'disk.SQLiteWriter' :
-            #     # info['store']['args']['path'] += (os.sep + 'healthcare-io.db3')
-            #     pass
-            
-            
-            # if info['store']['type'] == 'disk.SQLiteWriter' : 
-            #         info['store']['args']['table'] = SYS_ARGS['parse'].strip().lower()
-            #         _info = json.loads(json.dumps(info['store']))
-            #         _info['args']['table']='logs'
-            # else:
-            #     #
-            #     # if we are working with no-sql we will put the logs in it (performance )?
-
-            #         info['store']['args']['doc'] = SYS_ARGS['parse'].strip().lower()
-            #         _info = json.loads(json.dumps(info['store']))
-            #         _info['args']['doc'] = 'logs'
-            #         logger = factory.instance(**_info)
 
 
-            # writer = factory.instance(**info['store'])
-            
-            #
-            # we need to have batches ready for this in order to run some of these queries in parallel
-            # @TODO: Make sure it is with a persistence storage (not disk .. not thread/process safe yet)
-            #   - Make sure we can leverage this on n-cores later on, for now the assumption is a single core
-            #
             BATCH_COUNT = 1 if 'batch' not in SYS_ARGS else int (SYS_ARGS['batch'])
             BATCH_COUNT = 1 if 'batch' not in SYS_ARGS else int (SYS_ARGS['batch'])
             
             
             files = np.array_split(files,BATCH_COUNT)
             files = np.array_split(files,BATCH_COUNT)
@@ -304,27 +322,7 @@ if __name__ == '__main__' :
             while len(procs) > 0 :
             while len(procs) > 0 :
                 procs = [proc for proc in procs if proc.is_alive()]
                 procs = [proc for proc in procs if proc.is_alive()]
                 time.sleep(2)
                 time.sleep(2)
-            #     for filename in files :
-                    
-            #         if filename.strip() == '':
-            #             continue
-            #         # content,logs = get_content(filename,CONFIG,CONFIG['SECTION'])  
-            #         # 
-            #         try:              
-            #             content,logs = parse(filename = filename,type=SYS_ARGS['parse'])
-            #             if content :                        
-            #                 writer.write(content)
-            #             if logs :
-            #                 [logger.write(dict(_row,**{"parse":SYS_ARGS['parse']})) for _row in logs]
-            #             else:
-            #                 logger.write({"parse":SYS_ARGS['parse'],"name":filename,"completed":True,"rows":len(content)})
-            #         except Exception as e:
-            #             logger.write({"parse":SYS_ARGS['parse'],"filename":filename,"completed":False,"rows":-1,"msg":e.args[0]})
-            #         # print ([filename,len(content)])
-            #         #
-            #         # @TODO: forward this data to the writer and log engine 
-            #         #
-            
+
                 
                 
 
 
         pass
         pass
@@ -337,103 +335,71 @@ if __name__ == '__main__' :
         
         
         # PATH= SYS_ARGS['config'] if 'config' in SYS_ARGS else os.sep.join([os.environ['HOME'],'.healthcareio','config.json'])
         # PATH= SYS_ARGS['config'] if 'config' in SYS_ARGS else os.sep.join([os.environ['HOME'],'.healthcareio','config.json'])
         
         
-        e = analytics.engine(os.sep.join([PATH,'config.json'])) #--@TODO: make the configuration file globally accessible
-        e.apply(type='claims',serialize=True)
-        SYS_ARGS['engine'] = e
+        if os.path.exists(os.sep.join([PATH,'config.json'])) :
+            e = analytics.engine(os.sep.join([PATH,'config.json'])) #--@TODO: make the configuration file globally accessible
+            e.apply(type='claims',serialize=True)
+            SYS_ARGS['engine'] = e
+            SYS_ARGS['config'] = json.loads(open(os.sep.join([PATH,'config.json'])).read())
+        else:
+            SYS_ARGS['config'] = {"owner":None,"store":None}
+
+        if 'args' not in SYS_ARGS['config'] :
+            SYS_ARGS['config']["args"] = {"batch":1,"resume":True,"folder":"/data"}
         
         
+        me = pd.DataFrame(smart.top.read(name='healthcare-io.py')).args.unique().tolist()        
+        SYS_ARGS['me'] = me[0] #-- This key will identify the current process
+
         pointer = lambda : server.app.run(host='0.0.0.0',port=PORT,debug=DEBUG,threaded=False)
         pointer = lambda : server.app.run(host='0.0.0.0',port=PORT,debug=DEBUG,threaded=False)
         pthread = Process(target=pointer,args=())
         pthread = Process(target=pointer,args=())
         pthread.start()
         pthread.start()
+    elif 'check-update' in SYS_ARGS :
+        _args = {"url":SYS_ARGS['url']}
+        try:
+            if os.path.exists(os.sep.join([PATH,'config.json'])) :
+                SYS_ARGS['config'] = json.loads((open(os.sep.join([PATH,'config.json']))).read())
+            else:
+                SYS_ARGS['config'] = {}
+            if 'version' in SYS_ARGS['config'] :
+                _args['version'] = SYS_ARGS['config']['version']
+            version = check(**_args)
+            _version = {"current":0.0}if 'version' not in SYS_ARGS['config'] else SYS_ARGS['config']['version']
+            if _version['current'] != version['current'] :
+                print ()
+                print ("You need to upgrade your system to version to ",version['current'])
+                print ("\t- signup (for new configuration)")
+                print ("\t- use pip to upgrade the codebase")
+            else:
+                print ()
+                print ("You are running the current configuraiton version ",_version['current'])
+        except Exception as e:
+            print (e)
+            pass
         
         
     elif 'export' in SYS_ARGS:
     elif 'export' in SYS_ARGS:
         #
         #
         # this function is designed to export the data to csv
         # this function is designed to export the data to csv
         #
         #
-        format = SYS_ARGS['format'] if 'format' in SYS_ARGS else 'csv'
-        format = format.lower()
-        if set([format]) not in ['xls','csv'] :
-            format = 'csv'
-        
-    else:
-        msg = """
-        cli:
-        
-            healthcare-io.py    --<[signup|init]> <email> --store <sqlite|mongo> [--batch <value>]
-            healthcare-io.py    --parse claims --folder <path> [--batch <value>]
-            healthcare-io.py    --parse remits --folder <path> [--batch <value>] [--resume]
-        
-        parameters :
-            --<[signup|init]>   signup or get a configuration file from a parsing server
-            --store             data store mongo or sqlite or mongodb
-            --resume            will attempt to resume if there was an interruption
-        """
-        print(msg)
-        pass
-    # """
-    # The program was called from the command line thus we are expecting 
-    #     parse   in [claims,remits]
-    #     config  os.sep.path.exists(path)
-    #     folder    os.sep.path.exists(path)
-    #     store   store ()
-    # """
-    # p = len( set(['store','config','folder']) & set(SYS_ARGS.keys())) == 3 and ('db' in SYS_ARGS or 'path' in SYS_ARGS)
-    # TYPE = {
-    #     'mongo':'mongo.MongoWriter',
-    #     'couch':'couch.CouchWriter',
-    #     'disk':'disk.DiskWriter'
-    # }
-    # INFO = {
-    #     '837':{'scope':'claims','section':'HL'},
-    #     '835':{'scope':'remits','section':'CLP'}
-    # }
-    # if p :
-    #     args = {}
-    #     scope = SYS_ARGS['config'][:-5].split(os.sep)[-1]
-    #     CONTEXT = INFO[scope]['scope']
-    #     #
-    #     # @NOTE:
-    #     # improve how database and data stores are handled.
-    #     if SYS_ARGS['store'] == 'couch' :
-    #         args = {'url': SYS_ARGS['url'] if 'url' in SYS_ARGS else 'http://localhost:5984'}
-    #         args['dbname'] = SYS_ARGS['db']
+        path = SYS_ARGS['config']
+        TYPE = SYS_ARGS['export'] if 'export' in SYS_ARGS else '835'
+        if not os.path.exists(path) or TYPE not in ['835','837']:
+            print (HELP_MESSAGE)
+        else:
+            #
+            # Let's run the export function  ..., This will push files into a data-store of choice Redshift, PostgreSQL, MySQL ...
+            #
+            _store = {"type":"sql.SQLWriter","args":json.loads( (open(path) ).read())}
             
             
-    #     elif SYS_ARGS ['store'] == 'mongo':
-    #         args = {'host':SYS_ARGS['host']if 'host' in SYS_ARGS else 'localhost:27017'}
-    #     if SYS_ARGS['store'] in ['mongo','couch']:
-    #         args['dbname'] = SYS_ARGS['db'] if 'db' in SYS_ARGS else 'claims_outcomes'
-    #         args['doc'] = CONTEXT
+            pipes = export.Factory.instance(type=TYPE,write_store=_store) #"inspect":0,"cast":0}})  
+            # pipes[0].run()
+            for thread in pipes:
+                thread.start()
+                time.sleep(1)
+            while pipes :
+                pipes = [thread for thread in pipes if thread.is_alive()]     
+                time.sleep(1)       
+
 
 
-    #     TYPE    = TYPE[SYS_ARGS['store']] 
-    #     writer  = factory.instance(type=TYPE,args=args)
-    #     if SYS_ARGS['store'] == 'disk':
-    #         writer.init(path = 'output-claims.json')
-    #     logger  = factory.instance(type=TYPE,args= dict(args,**{"doc":"logs"}))
-    #     files   = os.listdir(SYS_ARGS['folder'])
-    #     CONFIG  = json.loads(open(SYS_ARGS['config']).read())
-    #     SECTION = INFO[scope]['section']
         
         
-    #     for file in files :
-    #         if 'limit' in SYS_ARGS and files.index(file) == int(SYS_ARGS['limit']) :
-    #             break
-    #         else:
-    #             filename = os.sep.join([SYS_ARGS['folder'],file])
-                
-    #             try:
-    #                 content,logs = get_content(filename,CONFIG,SECTION)
-    #             except Exception as e:
-    #                 if sys.version_info[0] > 2 :
-    #                     logs = [{"filename":filename,"msg":e.args[0]}]
-    #                 else:
-    #                     logs = [{"filename":filename,"msg":e.message}]
-    #                 content = None
-    #             if content :
-                    
-    #                 writer.write(content)
-    #             if logs:
-                    
-    #                 logger.write(logs)
-            
-                
-    #     pass
-    # else:
-    #     print (__doc__)
+    else:
+        
+        print(HELP_MESSAGE)

+ 175 - 21
healthcareio/server/__init__.py

@@ -3,7 +3,78 @@ from healthcareio.params import SYS_ARGS
 import healthcareio.analytics
 import healthcareio.analytics
 import os
 import os
 import json
 import json
+import time
+import smart
+import transport
+import pandas as pd
+import numpy as np
+from healthcareio import x12
+from healthcareio.export import export
+from multiprocessing import Process
+# from flask_socketio import SocketIO, emit, disconnect,send
+from healthcareio.server import proxy
+PATH = os.sep.join([os.environ['HOME'],'.healthcareio','config.json'])
 app = Flask(__name__)
 app = Flask(__name__)
+# socket_ = SocketIO(app)
+
+def resume (files):
+    _args = SYS_ARGS['config']['store'].copy()
+    if 'mongo' in SYS_ARGS['config']['store']['type'] :
+        _args['type'] = 'mongo.MongoReader'
+        reader = transport.factory.instance(**_args)
+    _files = []
+    try:
+        pipeline = [{"$match":{"completed":{"$eq":True}}},{"$group":{"_id":"$name"}},{"$project":{"name":"$_id","_id":0}}]
+        _args = {"aggregate":"logs","cursor":{},"allowDiskUse":True,"pipeline":pipeline}
+        _files = reader.read(mongo = _args)
+        _files = [item['name'] for item in _files]
+    except Exception as e :
+        pass
+    
+    return list(set(files) - set(_files))
+        
+    
+
+def run ():
+    #
+    # let's get the files in the folder (perhaps recursively traverse them)
+    #
+    FILES = []
+    BATCH = int(SYS_ARGS['config']['args']['batch']) #-- number of processes (poorly named variable)
+
+    for root,_dir,f in os.walk(SYS_ARGS['config']['args']['folder']) :
+        if f :
+            FILES += [os.sep.join([root,name]) for name in f]
+    FILES = resume(FILES)
+    FILES = np.array_split(FILES,BATCH)
+    procs = []
+    for FILE_GROUP in FILES :
+        
+        FILE_GROUP = FILE_GROUP.tolist()
+        # logger.write({"process":index,"parse":SYS_ARGS['parse'],"file_count":len(row)})
+        # proc = Process(target=apply,args=(row,info['store'],_info,))
+        parser = x12.Parser(PATH) #os.sep.join([PATH,'config.json']))
+        parser.set.files(FILE_GROUP)
+        parser.start()
+        procs.append(parser)  
+    SYS_ARGS['procs']  = procs
+# @socket_.on('data',namespace='/stream')
+def push() :
+    _args = dict(SYS_ARGS['config']['store'].copy(),**{"type":"mongo.MongoReader"})
+    reader = transport.factory.instance(**_args)
+    pipeline = [{"$group":{"_id":"$parse","claims":{"$addToSet":"$name"}}},{"$project":{"_id":0,"type":"$_id","count":{"$size":"$claims"}}}]
+    _args = {"aggregate":"logs","cursor":{},"allowDiskUse":True,"pipeline":pipeline}
+    r = pd.DataFrame(reader.read(mongo=_args))
+    r = healthcareio.analytics.Apex.apply({"chart":{"type":"donut","axis":{"x":"count","y":"type"}},"data":r})
+    # emit("update",r,json=True)
+    return r
+# @socket_.on('connect')
+# def client_connect(**r):
+#     print ('Connection received')
+#     print (r)
+#     push()
+#     pass
+    
 @app.route("/favicon.ico")
 @app.route("/favicon.ico")
 def _icon():
 def _icon():
     return send_from_directory(os.path.join([app.root_path, 'static','img','logo.svg']),
     return send_from_directory(os.path.join([app.root_path, 'static','img','logo.svg']),
@@ -12,7 +83,7 @@ def _icon():
 def init():
 def init():
     e = SYS_ARGS['engine']
     e = SYS_ARGS['engine']
     sections = {"remits":e.info['835'],"claims":e.info['837']}
     sections = {"remits":e.info['835'],"claims":e.info['837']}
-    _args = {"sections":sections}
+    _args = {"sections":sections,"store":SYS_ARGS["config"]["store"],"owner":SYS_ARGS['config']['owner'],"args":SYS_ARGS["config"]["args"]}
     return render_template("index.html",**_args)
     return render_template("index.html",**_args)
 @app.route("/format/<id>/<index>",methods=['POST'])
 @app.route("/format/<id>/<index>",methods=['POST'])
 def _format(id,index):
 def _format(id,index):
@@ -21,43 +92,117 @@ def _format(id,index):
     key = '837' if id == 'claims' else '835'
     key = '837' if id == 'claims' else '835'
     index = int(index)
     index = int(index)
     # p = e.info[key][index]
     # p = e.info[key][index]
-    p = e.apply(type=id,index=index)
-    
-    #
+    p = e.filter(type=id,index=index)
+   
     r = []
     r = []
-    for item in p[0]['pipeline'] :
-        _item= dict(item)
-        del _item['sql']
-        del _item ['data']
-        
+    for item in p['pipeline'] :
+        _item= dict(item)       
         _item = dict(_item,**healthcareio.analytics.Apex.apply(item))
         _item = dict(_item,**healthcareio.analytics.Apex.apply(item))
+        del _item['data']
         if 'apex' in _item or 'html' in _item:
         if 'apex' in _item or 'html' in _item:
             r.append(_item)
             r.append(_item)
         
         
-    r = {"id":p[0]['id'],"pipeline":r}    
+        
+    r = {"id":p['id'],"pipeline":r}  
     return json.dumps(r),200
     return json.dumps(r),200
+
 @app.route("/get/<id>/<index>",methods=['GET'])
 @app.route("/get/<id>/<index>",methods=['GET'])
 def get(id,index):
 def get(id,index):
     e = SYS_ARGS['engine']
     e = SYS_ARGS['engine']
     key = '837' if id == 'claims' else '835'
     key = '837' if id == 'claims' else '835'
     index = int(index)
     index = int(index)
     # p = e.info[key][index]
     # p = e.info[key][index]
-    p = e.apply(type=id,index=index)
+    p = e.filter(type=id,index=index)
     r = {}
     r = {}
     for item in p[0]['pipeline'] :
     for item in p[0]['pipeline'] :
         
         
         _item= [dict(item)]
         _item= [dict(item)]
         
         
-        r[item['label']] = item['data'].to_dict(orient='record')
-        # del _item['sql']
-        # del _item ['data']
-    #     print (item['label'])
-    #     _item['apex'] = healthcareio.analytics.Apex.apply(item)
-    #     if _item['apex']:
-    #         r.append(_item)
-        
-    # r = {"id":p[0]['id'],"pipeline":r}
+        # r[item['label']] = item['data'].to_dict(orient='record')
+        r[item['label']] = item['data'].to_dict('record')
     return json.dumps(r),200
     return json.dumps(r),200
+
+@app.route("/reset",methods=["POST"])
+def reset():
+    return "1",200
+@app.route("/data",methods=['GET'])
+def get_data ():
+    """
+    This function will return statistical data about the services i.e general statistics about what has/been processed
+    """
+    HEADER = {"Content-type":"application/json"}
+    _args = SYS_ARGS['config']
+    options = dict(proxy.get.files(_args),**proxy.get.processes(_args))
+    return json.dumps(options),HEADER
+@app.route("/log/<id>",methods=["POST","PUT","GET"])
+def log(id) :
+    HEADER = {"Content-Type":"application/json; charset=utf8"}
+    if id == 'params' and request.method in ['PUT', 'POST']:
+        info = request.json
+        _args = {"batch":info['batch'] if 'batch' in info else 1,"resume":True}
+        #
+        # We should update the configuration 
+        SYS_ARGS['config']['args'] = _args
+        PATH = os.sep.join([os.environ['HOME'],'.healthcareio','config.json'])
+        write = lambda content: (open(PATH,'w')).write(json.dumps(content))
+        proc = Process(target=write,args=(SYS_ARGS['config'],))
+        proc.start()
+        return "1",HEADER
+    pass
+@app.route("/io/<id>",methods=['POST'])
+def io_data(id):
+    if id == 'params' :
+        _args = request.json
+        #
+        # Expecting batch,folder as parameters
+        _args = request.json
+        _args['resume'] = True
+        print (_args)
+        #
+        # We should update the configuration 
+        SYS_ARGS['config']['args'] = _args
+        # PATH = os.sep.join([os.environ['HOME'],'.healthcareio','config.json'])
+        try:
+            write = lambda content: (open(PATH,'w')).write(json.dumps(content))
+            proc = Process(target=write,args=(SYS_ARGS['config'],))
+            proc.start()
+            # proc.join()
+            return "1",200
+        except Exception as e :
+            return "0",403
+            pass
+    elif id == 'stop' :
+        stop()
+        pass
+    elif id == 'run' :
+        # run()
+        _args = {"args":SYS_ARGS['config']['args'],"store":SYS_ARGS["config"]["store"]}
+        proxy.run(_args)
+        return "1",200
+        pass
+        
+@app.route("/export")
+def export_form():
+    _args = {"context":SYS_ARGS['context']}
+    return render_template("store.html",**_args)
+@app.route("/export",methods=['POST','PUT'])
+def apply_etl():
+    _info = request.json
+    m = {'s3':'s3.s3Writer','mongo':'mongo.MongoWriter'}
+    if _info :
+        dest_args = {'type':m[_info['type']],"args": _info['content'] }
+        src_args = SYS_ARGS['config']['store']
+        # print (_args)
+        # writer = transport.factory.instance(**_args)
+        proxy.publish(src_args,dest_args)
+        return "1",405
+        
+    else:
+        return "0",404
+@app.route("/update")
+def update():
+    pass
+    return "0",405
 @app.route("/reload",methods=['POST'])
 @app.route("/reload",methods=['POST'])
 def reload():
 def reload():
     # e = SYS_ARGS['engine']
     # e = SYS_ARGS['engine']
@@ -74,11 +219,20 @@ if __name__ == '__main__' :
     PORT = int(SYS_ARGS['port']) if 'port' in SYS_ARGS else 5500
     PORT = int(SYS_ARGS['port']) if 'port' in SYS_ARGS else 5500
     DEBUG= int(SYS_ARGS['debug']) if 'debug' in SYS_ARGS else 0
     DEBUG= int(SYS_ARGS['debug']) if 'debug' in SYS_ARGS else 0
     SYS_ARGS['context'] = SYS_ARGS['context'] if 'context' in SYS_ARGS else ''
     SYS_ARGS['context'] = SYS_ARGS['context'] if 'context' in SYS_ARGS else ''
+
     #
     #
     # 
     # 
     PATH= SYS_ARGS['config'] if 'config' in SYS_ARGS else os.sep.join([os.environ['HOME'],'.healthcareio','config.json'])
     PATH= SYS_ARGS['config'] if 'config' in SYS_ARGS else os.sep.join([os.environ['HOME'],'.healthcareio','config.json'])
+    #
+    # Adjusting configuration with parameters (batch,folder,resume)
+    if 'args' not in SYS_ARGS['config'] :
+        SYS_ARGS['config']["args"] = {"batch":1,"resume":True,"folder":"/data"}
+    
+    SYS_ARGS['procs'] = []
+    
     
     
+    # SYS_ARGS['path'] = os.sep.join([os.environ['HOME'],'.healthcareio','config.json'])
     e = healthcareio.analytics.engine(PATH)
     e = healthcareio.analytics.engine(PATH)
-    # e.apply(type='claims',serialize=True)
+    e.apply(type='claims',serialize=False)
     SYS_ARGS['engine'] = e
     SYS_ARGS['engine'] = e
     app.run(host='0.0.0.0',port=PORT,debug=DEBUG,threaded=True)
     app.run(host='0.0.0.0',port=PORT,debug=DEBUG,threaded=True)

+ 3 - 2
healthcareio/server/index.py

@@ -12,8 +12,9 @@ def _icon():
 def init():
 def init():
     e = SYS_ARGS['engine']
     e = SYS_ARGS['engine']
     sections = {"remits":e.info['835'],"claims":e.info['837']}
     sections = {"remits":e.info['835'],"claims":e.info['837']}
-    _args = {"sections":sections}
-    return render_template("index.html",**_args)
+    _args = {"sections":sections,"store":SYS_ARGS['config']['store']}
+    print (SYS_ARGS['config']['store'])
+    return render_template("setup.html",**_args)
 @app.route("/format/<id>/<index>",methods=['POST'])
 @app.route("/format/<id>/<index>",methods=['POST'])
 def _format(id,index):
 def _format(id,index):
     
     

+ 170 - 0
healthcareio/server/proxy.py

@@ -0,0 +1,170 @@
+"""
+    This file serves as proxy to healthcare-io, it will be embedded into the API
+"""
+import os
+import transport
+import numpy as np
+from healthcareio import x12
+import pandas as pd
+import smart
+from healthcareio.analytics import Apex
+import time
+class get :
+    PROCS = []
+    PATH = os.sep.join([os.environ['HOME'],'.healthcareio','config.json'])
+    @staticmethod
+    def resume (files,args):
+        """
+        This function will determine the appropriate files to be processed by performing a simple complementary set operation against the logs
+        @TODO: Support data-stores other than mongodb
+        :param files   list of files within a folder
+        :param _args    configuration
+        """
+        _args = args['store'].copy()
+        if 'mongo' in _args['type'] :
+            _args['type'] = 'mongo.MongoReader'
+            reader = transport.factory.instance(**_args)
+        _files = []
+        try:
+            pipeline = [{"$match":{"completed":{"$eq":True}}},{"$group":{"_id":"$name"}},{"$project":{"name":"$_id","_id":0}}]
+            _args = {"aggregate":"logs","cursor":{},"allowDiskUse":True,"pipeline":pipeline}
+            _files = reader.read(mongo = _args)
+            _files = [item['name'] for item in _files]
+        except Exception as e :
+            pass
+        print ( [len(list(set(files) - set(_files))),' files to be processed'])
+        return list(set(files) - set(_files))
+
+    @staticmethod
+    def processes(_args):
+        _info = pd.DataFrame(smart.top.read(name='healthcare-io.py'))[['name','cpu','mem']]
+        
+        if _info.shape[0] == 0 :
+            _info = pd.DataFrame({"name":["healthcare-io.py"],"cpu":[0],"mem":[0]})
+        # _info = pd.DataFrame(_info.groupby(['name']).sum())
+        # _info['name'] = ['healthcare-io.py']
+        m = {'cpu':'CPU','mem':'RAM','name':'name'}
+        _info.columns = [m[name] for name in _info.columns.tolist()]
+        _info.index = np.arange(_info.shape[0])
+
+        charts = []
+        for label in ['CPU','RAM'] :
+            value = _info[label].sum()
+            df = pd.DataFrame({"name":[label],label:[value]})
+            charts.append (
+                Apex.apply(
+                    {"data":df, "chart":{"type":"radial","axis":{"x":label,"y":"name"}}}
+                    )['apex']
+                )
+        #
+        # This will update the counts for the processes, upon subsequent requests so as to show the change
+        #     
+        N = 0
+        lprocs = []
+        for proc in get.PROCS :
+            if proc.is_alive() :
+                lprocs.append(proc)
+        N = len(lprocs)     
+        get.PROCS = lprocs
+        return {"process":{"chart":charts,"counts":N}}
+    @staticmethod
+    def files (_args):
+        _info = smart.folder.read(path='/data')
+        N = _info.files.tolist()[0]
+        if 'mongo' in _args['store']['type'] :
+            store_args = dict(_args['store'].copy(),**{"type":"mongo.MongoReader"})
+            # reader = transport.factory.instance(**_args)
+            
+            pipeline = [{"$group":{"_id":"$name","count":{"$sum":{"$cond":[{"$eq":["$completed",True]},1,0]}} }},{"$group":{"_id":None,"count":{"$sum":"$count"}}},{"$project":{"_id":0,"status":"completed","count":1}}]
+            query = {"mongo":{"aggregate":"logs","allowDiskUse":True,"cursor":{},"pipeline":pipeline}}
+            # _info = pd.DataFrame(reader.read(mongo={"aggregate":"logs","allowDiskUse":True,"cursor":{},"pipeline":pipeline}))
+            pipeline = [{"$group":{"_id":"$parse","claims":{"$addToSet":"$name"}}},{"$project":{"_id":0,"type":"$_id","count":{"$size":"$claims"}}}]
+            _query = {"mongo":{"aggregate":"logs","cursor":{},"allowDiskUse":True,"pipeline":pipeline}} #-- distribution claims/remits
+
+
+        else:
+            store_args = dict(_args['store'].copy(),**{"type":"disk.SQLiteReader"})
+            store_args['args']['table'] = 'logs'
+            query= {"sql":"select count(distinct json_extract(data,'$.name')) as count, 'completed' status from logs where json_extract(data,'$.completed') = true"}
+            _query={"sql":"select json_extract(data,'$.parse') as type,count(distinct json_extract(data,'$.name')) as count from logs group by type"} #-- distribution claim/remits
+        reader = transport.factory.instance(**store_args)
+        _info = pd.DataFrame(reader.read(**query))
+        if not _info.shape[0] :
+            _info = pd.DataFrame({"status":["completed"],"count":[0]})
+        _info['count'] = np.round( (_info['count'] * 100 )/N,2)
+        
+        charts = [Apex.apply({"data":_info,"chart":{"type":"radial","axis":{"y":"status","x":"count"}}})['apex']]
+        #
+        # Let us classify the files now i.e claims / remits
+        #
+        
+        
+        # pipeline = [{"$group":{"_id":"$parse","claims":{"$addToSet":"$name"}}},{"$project":{"_id":0,"type":"$_id","count":{"$size":"$claims"}}}]
+        # _args = {"aggregate":"logs","cursor":{},"allowDiskUse":True,"pipeline":pipeline}
+        # r = pd.DataFrame(reader.read(mongo=_args))
+        r = pd.DataFrame(reader.read(**_query)) #-- distribution claims/remits
+        r = Apex.apply({"chart":{"type":"donut","axis":{"x":"count","y":"type"}},"data":r})['apex']
+        r['chart']['height'] = '100%'
+        r['legend']['position'] = 'bottom'
+
+        charts += [r]
+
+        
+        return {"files":{"counts":N,"chart":charts}}
+
+        pass
+#
+# Process handling ....
+
+
+def run (_args) :
+    """
+    This function will run the jobs and insure as processes (as daemons).
+    :param _args    system configuration
+    """
+    FILES = []
+    BATCH = int(_args['args']['batch']) #-- number of processes (poorly named variable)
+
+    for root,_dir,f in os.walk(_args['args']['folder']) :
+        if f :
+            FILES += [os.sep.join([root,name]) for name in f]
+    FILES = get.resume(FILES,_args)
+    FILES = np.array_split(FILES,BATCH)
+    
+    for FILE_GROUP in FILES :
+        
+        FILE_GROUP = FILE_GROUP.tolist()
+        # logger.write({"process":index,"parse":_args['parse'],"file_count":len(row)})
+        # proc = Process(target=apply,args=(row,info['store'],_info,))
+        parser = x12.Parser(get.PATH) #os.sep.join([PATH,'config.json']))
+        parser.set.files(FILE_GROUP)   
+        parser.daemon = True
+        parser.start()
+        get.PROCS.append(parser)     
+        time.sleep(3)
+    #
+    # @TODO:consider submitting an update to clients via publish/subscribe framework
+    #
+    return get.PROCS
+def stop(_args):
+    for job in get.PROCS :
+        if job.is_alive() :
+            job.terminate()
+    get.PROCS = []
+    #
+    # @TODO: consider submitting an update to clients via publish/subscribe framework
+    pass
+def write(src_args,dest_args,files) :
+    #
+    # @TODO: Support for SQLite
+    pass
+def publish (src_args,dest_args,folder="/data"):
+    FILES = []
+    for root,_dir,f in os.walk(folder) :
+        if f :
+            FILES += [os.sep.join([root,name]) for name in f]
+    #
+    # @TODO: Add support for SQLite ....
+    
+    FILES = np.array_split(FILES,4)
+    

+ 49 - 0
healthcareio/server/static/css/default.css

@@ -1,3 +1,4 @@
+
 .active {
 .active {
     padding:4px;
     padding:4px;
     cursor:pointer;
     cursor:pointer;
@@ -6,3 +7,51 @@
 .active:hover{
 .active:hover{
     border-bottom:2px solid #ff6500; 
     border-bottom:2px solid #ff6500; 
 }
 }
+input[type=text]{
+    border:1px solid transparent;
+    background-color:#f3f3f3;
+    outline: 0px;
+    padding:8px;
+    font-weight:normal;
+    font-family:sans-serif;
+    color:black;
+}
+.active-button {
+    display:grid;
+    grid-template-columns: 32px auto;
+    gap:2px;
+    align-items:center;
+    border:2px solid #CAD5E0;
+    cursor:pointer;
+    
+}
+.active-button i {padding:4px;;}
+.active-button:hover { border-color:#ff6500}
+
+.system {display:grid; grid-template-columns: 45% 1px auto; gap:20px;  margin-left:5%; width:90%;}
+.system .status .item {display:grid; grid-template-columns: 75px 8px auto; gap:2px;}
+.input-form {display:grid; gap:2px;}
+.input-form .item {display:grid; grid-template-columns: 125px auto; gap:2px; align-items:center;}
+.input-form .item .label { font-weight:bold; padding-left:10px}
+.fa-cog {color:#4682B4}
+.fa-check {color:#00c6b3}
+.fa-times {color:maroon}
+
+.code {
+    margin:4px;
+    background:#000000 ;
+    padding:8px;
+    font-family: 'Courier New', Courier, monospace;
+    color:#d3d3d3;
+    font-size:12px;
+    line-height: 2;
+}
+
+.tabs {display:grid; grid-template-columns: repeat(3,1fr) auto; gap:0px; align-items:center; text-align: center;}
+.tab {border:1px solid transparent; border-bottom-color:#D3D3D3; font-weight:bold; padding:4px}
+
+.tabs .selected {border-color:#CAD5E0; border-bottom-color:transparent; }
+.system iframe {width:100%; height:100%; border:1px solid transparent;}
+.data-info {height:90%; padding:8px;}
+.fa-cloud {color:#4682B4}
+.fa-database{color:#cc8c91}

+ 21 - 0
healthcareio/server/static/dialog.html

@@ -0,0 +1,21 @@
+<div class="dialog">
+    <div class="title-bar">
+        <div class="title bold" ></div>
+        <div class="active close" align="center"><i class="fas fa-times"></i></div>
+    </div>
+    <div class="message">
+        <div class="icon" align="center">
+            <i id="msg-icon"></i>
+        </div>
+        <div class="text"></div>
+    </div>
+
+    <div class="action">
+        <div class="active-button border-round" align="center">
+            <div align="center">
+                <i class="fas fa-check"></i>
+            </div>
+            <div class="bold">Ok</div>
+        </div>
+    </div>
+</div>

BIN
healthcareio/server/static/img/smart-top.png


+ 75 - 0
healthcareio/server/static/js/io/dialog.js

@@ -0,0 +1,75 @@
+/***
+ * This file will handle the dialog boxes as they and their associated configurations and function binding
+ */
+if (!dialog){
+    var dialog = {}
+}
+
+dialog.open = function(title,msg,pointer){
+    if (sessionStorage.dialog == null){
+
+    
+    var http = HttpClient.instance()
+        http.get(sessionStorage.io_context+'/static/dialog.html',function(x){
+            var html = x.responseText
+            jx.modal.show({html:html,id:'dialog'})
+            $('.dialog .title').text(title)            
+            $('.dialog .message .text').text(msg)
+            dialog.status.ask()
+            $('.dialog .action .active-button').on('click',pointer)
+            $('.dialog .title-bar .close').on('click',function(){dialog.close(0)})
+
+        })
+    }else{
+        var html = sessionStorage.dialog
+        jx.modal.show({html:html,id:'dialog'})
+        dialog.status.ask()
+        $('.dialog .action .active-button').on('click',pointer)
+        $('.dialog .title-bar .close').on('click',function(){dialog.close(0)})
+
+    }
+}
+dialog.bind = function(pointer){
+    if (pointer == null){
+        pointer = dialog.close
+    }
+    $('.dialog .action .active-button').off()
+    $('.dialog .action .active-button').on('click',pointer)
+}
+dialog.close = function(delay){
+    delay = (delay == null)?1750:delay
+    setTimeout(function(){
+        if ( $('.dialog').length > 0){
+            jx.modal.close()
+        }
+    },delay)
+}
+dialog.status = {}
+dialog.status.wait = function(){
+    $('.dialog .action .active-button').hide()
+}
+dialog.status.confirm = function(){
+    $('.dialog .action .active-button').show()
+}
+dialog.status.busy = function(){
+    $('.dialog .message #msg-icon').removeClass()
+    $('.dialog .message #msg-icon').addClass('fas fa-cog fa-4x fa-spin')
+    
+}
+dialog.status.fail = function(){
+    $('.dialog .message #msg-icon').removeClass()
+    $('.dialog .message #msg-icon').addClass('fas fa-times fa-4x')
+}
+dialog.status.ask = function(){
+    $('.dialog .message #msg-icon').removeClass()
+    $('.dialog .message #msg-icon').addClass('far fa-question-circle fa-4x')
+}
+dialog.status.warn = function(){
+    $('.dialog .message #msg-icon').removeClass()
+    $('.dialog .message #msg-icon').addClass('fas fa-exclamation-triangle fa-4x')
+}
+dialog.status.success = function(){
+    $('.dialog .message #msg-icon').removeClass()
+    $('.dialog .message #msg-icon').addClass('fas fa-check fa-4x')
+}
+

+ 15 - 0
healthcareio/server/static/js/io/healthcare.js

@@ -0,0 +1,15 @@
+if (!healthcare) {
+    var healthcare = {io:{}}
+}
+healthcare.io = {'dialog':dialog,'confirmed':confirmed,'reset':reset,'update':update,'run':run,'publish':publish}
+healthcare.io.apply = function(){
+    var value = $('.input-form .item .procs').val()    
+    var folder= $('.input-form .item .folder').val()
+    $('.code .batch').html(value)
+    var http = HttpClient.instance()
+    http.setData({"batch":value,"resume":true,"folder":folder},"application/json")
+    http.post(sessionStorage.io_context+'/io/params',function(x){})
+}
+
+
+

+ 182 - 0
healthcareio/server/static/js/io/io.js

@@ -0,0 +1,182 @@
+/**
+ * This file will depend on dialog.js (soft dependency). Some functions here will make calls to resources in dialog.js
+ */
+
+var reset = function(){
+    dialog.open('Healthcare/IO::Parser', 'Are you sure you would like to delete all data parsed? Click Ok to confirm',confirmed.reset)
+    
+}
+var update= function(){
+    dialog.open('Healthcare/IO::Parser','Update will change parsing configuration. Would you like to continue ?',confirmed.update)
+}
+var run = function(){
+    dialog.open('Healthcare/IO::Parser','Preparing parser, confirm to continue',confirmed.run)
+}
+var _queue = {socket:null}
+var confirmed = {}
+confirmed.run = function(){
+    dialog.status.busy()
+    dialog.status.wait()
+    $('.dialog .message .text').html('Initiating Parsing ...')
+    setTimeout(function(){
+        var http = HttpClient.instance()
+        http.post(sessionStorage.io_context+'/io/run',function(x){
+        //    dialog.handler = setInterval(function(){monitor.data()},750)
+        monitor.data()
+        //dialog.close()
+    
+        })
+    
+    },1000)
+}
+confirmed.reset = function(){
+    var uri = sessionStorage.io_context+'/reset'
+    var http= HttpClient.instance()
+    dialog.status.busy()
+    dialog.status.wait()
+    http.post(uri,function(x){
+       setTimeout(function(){
+        if (x.status == 200 && x.responseText == "1"){
+            dialog.status.success()
+            $('.dialog .message .text').html('Reset Healthcare/IO::Parser was successful!<br><div align="center">Dialog will be closing</div>')
+            dialog.close()
+        }else{
+            dialog.status.fail()
+            
+            
+        }
+
+       },2000)
+    })
+}
+
+confirmed.update = function(){
+    var uri = sessionStorage.io_context+'/update'
+    var email  = $('#email').val()
+    //
+    //-- validate the email
+    if (email.match(/^([^\s]+)@([^\s@]+)\.(org|com|edu|io)$/i)){
+        dialog.status.wait()
+        dialog.status.busy()
+        var http = HttpClient.instance()
+        http.setData({"email":email},"application/son")
+        setTimeout(function(){
+            http.post(uri,function(x){
+                if(x.status == 200 && x.responseText == "1"){
+                    dialog.status.success()
+                }else{
+                    
+                    dialog.status.fail()
+                    $('.dialog .message .text').html('Error code '+x.status)
+                    dialog.bind()
+                    dialog.status.confirm()
+                    $('.dialog .title-bar .title').html("Error found")
+            
+                }
+            })
+    
+        },1000)
+    }else{
+        dialog.status.fail()
+        dialog.bind()
+        $('.dialog .title-bar .title').text("Error found")
+        $('.dialog .message .text').html('Invvalid Email entered')
+        dialog.status.confirm()
+    }
+    
+}
+
+/**
+ * This namespace is designed to export data to either the cloud or to a database
+ */
+var publish={set:{}}
+publish.post = function(){
+    
+    if($('.jxmodal').length > 0){
+        jx.modal.close()
+    }
+    dialog.open('Export/ETL','Please wait')
+    dialog.status.busy()
+
+    var http = HttpClient.instance()
+    http.setData(JSON.parse(sessionStorage.export),"application/json")
+    http.post(sessionStorage.io_context+'/export',function(x){
+        if (x.status != 200){
+            setTimeout(function(){
+                $('.dialog .message .text').html('An error occurred with code '+x.status)
+                dialog.status.fail()
+                dialog.status.wait()
+    
+            },1500)
+            
+        }
+        //
+        // @TODO: Have progress be monitored for this bad boy i.e open the connection to socket and read in ...
+        //
+    })
+
+}
+publish.set.file = function(){
+    var file = $('#file')[0].files[0]
+    $('.file .name').html(file.name)
+    var button = $('.cloud input').prop('disabled',true)
+    var div = $('.cloud .file .fa-file-upload')[0]
+    $(div).empty()
+    $(div).addClass('fas fa-cog fa-spin')
+    var reader = new FileReader()
+    reader.readAsText(file)
+    
+    
+    reader.onload = function(){
+        _args = {"type":$('.cloud .id').html().trim(),"content":reader.result}
+        // _args = JSON.stringify(_args)
+        if (_args.content.match(/^\{.+/i) == null){
+            content = _args.content.split('\n')[1].split(',')
+            _args.content = {'bucket':'healthcareio','access_key':content[0].trim(),'secret_key':content[1].trim()}
+        }
+        sessionStorage.export = JSON.stringify(_args)
+    }
+    
+    reader.onloadend = function(){
+        setTimeout(function(){
+            var div = $('.cloud .file .fa-cog')[0]
+            $(div).empty()
+            $(div).addClass('fas fa-check')
+            $(div).removeClass('fa-spin')
+            // jx.modal.close()
+            
+            //setTimeout(jx.modal.close,1500)
+            publish.post()
+        },2000)    
+    }
+}
+publish.database = {}
+
+publish.database.init = function(id){
+    //
+    // we are expecting id in {mongo,couch,postgresql,mysql,sqlite}
+    // @TODO: Account for cloud service brokers like dropbox, box, one-drive and google-drive
+    sessionStorage.export = "{}"
+    p = {'id':id}
+    if (id.match(/(mongodb|postgresql|mysql|sqlite|couchdb)/i)){
+        var hide_id = '.store .cloud'
+        var show_id = '.store .database'
+    }else{
+        //
+        // @TODO: generate an error message
+        var show_id = '.store .cloud'
+        var hide_id = '.store .database'
+
+    }
+    var http = HttpClient.instance()
+    http.get(sessionStorage.io_context+'/export',function(x){
+        var html = x.responseText
+        jx.modal.show({'html':html,'id':'dialog'})
+        $(hide_id).hide(function(){
+            $(show_id).show()
+        })
+
+        $('.store .id').text(id)
+    })
+
+}

+ 125 - 116
healthcareio/server/static/js/jx/rpc.js

@@ -12,119 +12,128 @@
 *	Improve on how returned data is handled (if necessary).
 *	Improve on how returned data is handled (if necessary).
 */
 */
 if(!jx){
 if(!jx){
-  var jx = {}
-}
-/**
- * These are a few parsers that can come in handy:
- * urlparser:	This parser is intended to break down a url parameter string in key,value pairs
- */
-
-function urlparser(url){
-        if(url.toString().match(/\x3F/) != null){
-            url = url.split('\x3F')[1]
-            
-        }
-	var p = url.split('&') ;
-	var r = {} ;
-	r.meta = [] ;
-	r.data = {} ;
-	var entry;
-	for(var i=0; i < p.length; i++){
-		entry = p[i] ;
-		key 	= (entry.match('(.*)=')  !=null)? entry.match('(.*)=')[1]:null ;
-		value 	= (entry.match('=(.*)$') != null)? entry.match('=(.*)$')[1]:null
-		if(key != null){
-			key = key.replace('\x3F','')
-			r.meta.push(key) ;
-			r.data[key] = value  ;
-		}
-	}
-
-	return r.data;
-}
-
-/**
-* The following are corrections related to consistency in style & cohesion
-*/
-jx.ajax = {}
-jx.ajax.get = {} ;
-jx.ajax.debug = null;
-jx.ajax.get.instance = function(){
-    var factory = function(){
-        this.obj = {}
-        this.obj.headers = {}
-        this.obj.async  = true;
-        this.setHeader = function(key,value){
-            if(key.constructor != String && value == null){
-                this.obj.headers = key ;
-            }else{
-                this.obj.headers[key] = value;
-            }
-        }
-        this.setData = function(data){
-            this.obj.data = data;
-        }
-        this.setAsync = function(flag){
-            this.obj.async = (flag == true) ;
-        }
-        this.send = function(url,callback,method){
-            
-            if(method == null){
-                method = 'GET'
-            }
-            
-            p = jx.ajax.debug != null;
-            q = false;
-            if(p){
-                q = jx.ajax.debug[url] != null;
-            }
-            
-            is_debuggable = p && q
-            
-            if(is_debuggable){
-                x = {} ;
-                x.responseText = jx.ajax.debug [url] ;                
-                callback(x)
-            }else{
-                    var http =  new XMLHttpRequest()  ;
-                    http.onreadystatechange = function(){
-                        if(http.readyState == 4){
-                            
-                            callback(http)
-                        }
-                    }
-                    //
-                    // In order to insure backward compatibility
-                    // Previous versions allowed the user to set the variable on the wrapper (poor design)
-                    if(this.async != null){
-                        this.setAsync(this.async) ;
-                    }
-                    http.open(method,url,this.obj.async) ;
-                    for(key in this.obj.headers){
-                        value = this.obj.headers[key] ;
-                        
-                        http.setRequestHeader(key,value)
-                    }
-                    
-                    http.send(this.obj.data)
-            }
-            
-            
-        }
-	this.put = function(url,callback){
-		this.send(url,callback,'PUT') ;
-	}
-	this.get = function(url,callback){
-		this.send(url,callback,'GET') ;
-	}
-	this.post = function(url,callback){
-		this.send(url,callback,'POST') ;
-	}
-    }//-- end of the factory method
-    return new factory() ;
-}
-
-//
-// backward compatibility
-jx.ajax.getInstance = jx.ajax.get.instance ;
-var HttpClient = jx.ajax.get ;
+    var jx = {}
+  }
+  /**
+   * These are a few parsers that can come in handy:
+   * urlparser:	This parser is intended to break down a url parameter string in key,value pairs
+   */
+  
+  function urlparser(url){
+          if(url.toString().match(/\x3F/) != null){
+              url = url.split('\x3F')[1]
+              
+          }
+      var p = url.split('&') ;
+      var r = {} ;
+      r.meta = [] ;
+      r.data = {} ;
+      var entry;
+      for(var i=0; i < p.length; i++){
+          entry = p[i] ;
+          key 	= (entry.match('(.*)=')  !=null)? entry.match('(.*)=')[1]:null ;
+          value 	= (entry.match('=(.*)$') != null)? entry.match('=(.*)$')[1]:null
+          if(key != null){
+              key = key.replace('\x3F','')
+              r.meta.push(key) ;
+              r.data[key] = value  ;
+          }
+      }
+  
+      return r.data;
+  }
+  
+  /**
+  * The following are corrections related to consistency in style & cohesion
+  */
+  jx.ajax = {}
+  jx.ajax.get = {} ;
+  jx.ajax.debug = null;
+  jx.ajax.get.instance = function(){
+      var factory = function(){
+          this.obj = {}
+          this.obj.headers = {}
+          this.obj.async  = true;
+          this.setHeader = function(key,value){
+              if(key.constructor != String && value == null){
+                  this.obj.headers = key ;
+              }else{
+                  this.obj.headers[key] = value;
+              }
+          }
+          this.setData = function(data,mimetype){
+              if(mimetype == null)
+                  this.obj.data = data;
+              else {
+                  this.obj.headers['Content-Type'] = mimetype
+                  if(mimetype.match(/application\/json/i)){
+                      this.obj.data = JSON.stringify(data)
+                  }
+              }
+              
+          }
+          this.setAsync = function(flag){
+              this.obj.async = (flag == true) ;
+          }
+          this.send = function(url,callback,method){
+              
+              if(method == null){
+                  method = 'GET'
+              }
+              
+              p = jx.ajax.debug != null;
+              q = false;
+              if(p){
+                  q = jx.ajax.debug[url] != null;
+              }
+              
+              is_debuggable = p && q
+              
+              if(is_debuggable){
+                  x = {} ;
+                  x.responseText = jx.ajax.debug [url] ;                
+                  callback(x)
+              }else{
+                      var http =  new XMLHttpRequest()  ;
+                      http.onreadystatechange = function(){
+                          if(http.readyState == 4){
+                              
+                              callback(http)
+                          }
+                      }
+                      //
+                      // In order to insure backward compatibility
+                      // Previous versions allowed the user to set the variable on the wrapper (poor design)
+                      if(this.async != null){
+                          this.setAsync(this.async) ;
+                      }
+                      http.open(method,url,this.obj.async) ;
+                      for(key in this.obj.headers){
+                          value = this.obj.headers[key] ;
+                          
+                          http.setRequestHeader(key,value)
+                      }
+                      
+                      http.send(this.obj.data)
+              }
+              
+              
+          }
+      this.put = function(url,callback){
+          this.send(url,callback,'PUT') ;
+      }
+      this.get = function(url,callback){
+          this.send(url,callback,'GET') ;
+      }
+      this.post = function(url,callback){
+          this.send(url,callback,'POST') ;
+      }
+      }//-- end of the factory method
+      return new factory() ;
+  }
+  
+  //
+  // backward compatibility
+  jx.ajax.getInstance = jx.ajax.get.instance ;
+  var HttpClient = jx.ajax.get ;
+  

+ 29 - 0
healthcareio/server/templates/header.html

@@ -0,0 +1,29 @@
+<meta charset="utf8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+<meta http-equiv="Pragma" content="no-cache" />
+<meta http-equiv="Expires" content="0" />
+<link rel="shortcut icon" href="{{context}}/static/img/logo.svg" type="image/icon type">
+<link rel="stylesheet" href="{{context}}/static/css/default.css" type="text/css">
+<script src="{{context}}/static/js/jx/rpc.js"></script>
+<script src="{{context}}/static/js/jx/dom.js"></script>
+<script src="{{context}}/static/js/jx/utils.js"></script>
+
+<script src="{{context}}/static/js/jquery.js"></script>
+
+<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
+<link href="{{context}}/static/css/borders.css" type="text/css" rel="stylesheet">
+<link href="{{context}}/static/css/fa/css/all.css" type="text/css" rel="stylesheet">
+<script src="{{context}}/static/css/fa/js/all.js"></script>
+
+<div class="border-round border menu-bar">
+    <div class="menu">
+       Admin
+       <div class="menu-items border">
+           <div class="item">Setup</div>
+       </div>
+    </div>
+    <div class="menu">
+        Claims & Remits
+    </div>
+</div>

+ 90 - 33
healthcareio/server/templates/index.html

@@ -8,6 +8,7 @@
 <script src="{{context}}/static/js/jx/rpc.js"></script>
 <script src="{{context}}/static/js/jx/rpc.js"></script>
 <script src="{{context}}/static/js/jx/dom.js"></script>
 <script src="{{context}}/static/js/jx/dom.js"></script>
 <script src="{{context}}/static/js/jx/utils.js"></script>
 <script src="{{context}}/static/js/jx/utils.js"></script>
+<script src="{{context}}/static/js/jx/ext/modal.js"></script>
 
 
 <script src="{{context}}/static/js/jquery.js"></script>
 <script src="{{context}}/static/js/jquery.js"></script>
 
 
@@ -15,6 +16,9 @@
 <link href="{{context}}/static/css/borders.css" type="text/css" rel="stylesheet">
 <link href="{{context}}/static/css/borders.css" type="text/css" rel="stylesheet">
 <link href="{{context}}/static/css/fa/css/all.css" type="text/css" rel="stylesheet">
 <link href="{{context}}/static/css/fa/css/all.css" type="text/css" rel="stylesheet">
 <script src="{{context}}/static/css/fa/js/all.js"></script>
 <script src="{{context}}/static/css/fa/js/all.js"></script>
+<script src="{{context}}/static/js/io/dialog.js"></script>
+<script src="{{context}}/static/js/io/io.js"></script>
+<script src="{{context}}/static/js/io/healthcare.js"></script>
 <style>
 <style>
     body {
     body {
         font-size:16px;
         font-size:16px;
@@ -64,6 +68,7 @@
         scroll-behavior: smooth;
         scroll-behavior: smooth;
         gap:2px;
         gap:2px;
         padding:4px;
         padding:4px;
+        height:95%;
         
         
 
 
     }
     }
@@ -81,12 +86,12 @@
         
         
 
 
     }
     }
-    .dashboard .chart-pane .chart {
+    .dashboard .chart-pane .chart2 {
         max-height:99%;
         max-height:99%;
         min-height:99%;
         min-height:99%;
         height:99%;
         height:99%;
     }
     }
-    .dashboard .chart-pane .chart .graph {
+    .dashboard .chart-pane .chart2 .graph {
         
         
         
         
         max-height:100%;
         max-height:100%;
@@ -95,7 +100,7 @@
 
 
 
 
     }
     }
-    .dashboard .chart-pane .chart .graph .apexcharts-svg {
+    .dashboard .chart-pane .chart2 .graph .apexcharts-svg {
         /*border-radius:8px;*/
         /*border-radius:8px;*/
         background-color:#f3f3f3;
         background-color:#f3f3f3;
         max-height:100%;
         max-height:100%;
@@ -161,7 +166,7 @@
     }
     }
    
    
 
 
-    .gradient {  background-image: linear-gradient(to top,#F3F3F3, #D3D3D3, #F3F3F3);}
+    .gradient {  background-image: linear-gradient(to top,#F3F3F3, #FFFFFF, #FFFFFF);}
     .white {color:#ffffff}
     .white {color:#ffffff}
     .scalar {
     .scalar {
         display:grid;
         display:grid;
@@ -172,23 +177,39 @@
         
         
         
         
         
         
+        
     }
     }
     .shadow {
     .shadow {
         box-shadow: 0px 4px 4px #d3d3d3;
         box-shadow: 0px 4px 4px #d3d3d3;
     }
     }
     .scalar-title { padding:8px; text-transform: capitalize; }
     .scalar-title { padding:8px; text-transform: capitalize; }
-    .scalar .value {font-size:32px; font-weight:bold; margin:4%;}
+    .scalar .value {font-size:32px; margin:4%;}
     .scalar .label {font-size:12px; text-transform:capitalize; text-overflow: ellipsis; display:grid; align-items: flex-end; text-align:center ;}
     .scalar .label {font-size:12px; text-transform:capitalize; text-overflow: ellipsis; display:grid; align-items: flex-end; text-align:center ;}
+    
+   
+    .monthly_patient_count, .taxonomy_code_distribution, .top_adjustment_codes, .adjustment_reasons {
+        grid-row:1 / span 3 ;
+        
+        
+    }
+
+
 </style>
 </style>
 <script>
 <script>
     sessionStorage.io_context  = "{{context}}"
     sessionStorage.io_context  = "{{context}}"
     var plot = function(id,index){
     var plot = function(id,index){
+        $('.system').slideUp(function(){
+            $('.dashboard').slideDown()
+        })
+        
         var uri = ([sessionStorage.io_context,'format',id,index]).join('/')
         var uri = ([sessionStorage.io_context,'format',id,index]).join('/')
         var httpclient = HttpClient.instance()
         var httpclient = HttpClient.instance()
         //
         //
         // @TODO: Let the user know something is going on .. spinner
         // @TODO: Let the user know something is going on .. spinner
         httpclient.post(uri,function(x){
         httpclient.post(uri,function(x){
+            
             var r = JSON.parse(x.responseText)
             var r = JSON.parse(x.responseText)
+            
             var pane = jx.dom.get.instance('DIV')
             var pane = jx.dom.get.instance('DIV')
             var scalar_pane = jx.dom.get.instance('DIV')
             var scalar_pane = jx.dom.get.instance('DIV')
             pane.id = r.id
             pane.id = r.id
@@ -202,9 +223,9 @@
             var p = jx.utils.patterns.visitor(r.pipeline,function(item){
             var p = jx.utils.patterns.visitor(r.pipeline,function(item){
                 var div = jx.dom.get.instance('DIV')
                 var div = jx.dom.get.instance('DIV')
                 var frame = jx.dom.get.instance('DIV')
                 var frame = jx.dom.get.instance('DIV')
-                //div.className = 'chart border-round border'
-                frame.className = 'chart border'
-                div.className = 'graph'
+                //div.className = 'chart2 border-round border'
+                frame.className = 'chart2 border ' + item.label.toLowerCase().replace(/ /g,'_')
+                div.className = 'graph '
                 //frame.append(div)
                 //frame.append(div)
                 //pane.append(frame)
                 //pane.append(frame)
                 
                 
@@ -212,8 +233,11 @@
              if(item.apex != null){   item.apex.title = {text:item.label}
              if(item.apex != null){   item.apex.title = {text:item.label}
                     frame.append(div)
                     frame.append(div)
                     pane.append(frame)
                     pane.append(frame)
-                    delete item.apex.colors
-                    item.apex.theme= {
+                    if (item.apex.colors ){
+                        delete item.apex.colors
+                    }
+                    
+                    /*item.apex.theme= {
                         mode: 'material', 
                         mode: 'material', 
                         palette: 'palette6', 
                         palette: 'palette6', 
                         monochrome: {
                         monochrome: {
@@ -222,8 +246,11 @@
                             shadeTo: 'light',
                             shadeTo: 'light',
                             shadeIntensity: 0.65
                             shadeIntensity: 0.65
                         },
                         },
-                    }
+                    }*/
                     item.apex.chart.height = '100%'
                     item.apex.chart.height = '100%'
+                    delete item.apex.chart.width
+                    
+                    
                     return new ApexCharts(div,item.apex)
                     return new ApexCharts(div,item.apex)
                 }else{
                 }else{
                     //frame.className = ''
                     //frame.className = ''
@@ -283,50 +310,80 @@
         }
         }
     }
     }
     $(document).ready(function(){
     $(document).ready(function(){
+        $('.dashabord').hide()
         $('.item-group').slideUp()
         $('.item-group').slideUp()
+        var index = 0;
+        jx.utils.patterns.visitor($('.menu .items'), function(_item){
+            var node = $(_item).children()[0]
+            $(node).attr('index',index)
+            node.onclick = function(){ toggle($(this).attr('index')) }
+            index += 1;
+            
+        })
+        var year = (new Date()).getFullYear()
+        $('.year').text(year)
     })
     })
 </script>
 </script>
 <title>Healthcare/IO Analytics</title>
 <title>Healthcare/IO Analytics</title>
 <body>
 <body>
-    <div class="pane border">
+    <div class="pane">
         <div class="header border-bottom">
         <div class="header border-bottom">
-            <div class="caption">Healthcare/IO</div>
-            <div class="small">Analytics Dashboard</div>
+            <div class="caption">Healthcare/IO :: Parser</div>
+            <div class="small">Dashboard</div>
         </div>
         </div>
         
         
-        <div class="menu border-right">
+        <div class="menu">
+            
            <div>
            <div>
+                <div class="items">
+                    <div class="bold active" style="margin:4px; height:28px; display:grid; gap:2px; grid-template-columns:auto 32px;"> 
+                        <div>Setup</div>
+                        <div align="center" class="glyph" >
+                            
+                            <i class="fas fa-angle-down"></i>
+                        </div>
+                    </div>
+                    <div class="item-group border border-round">
+                        <div class="item small active" onclick="setup.open()">Configure Parser</div>
+                        
+                        <div class="item small active" onclick="healthcare.io.reset()">Reset Parser</div>
+
+                    </div>
+                </div>
             {% for key in sections %}
             {% for key in sections %}
-            <div class="items">
-                <div class="bold active" onclick="toggle({{loop.index -1}})" style="margin:4px; height:28px; display:grid; gap:2px; grid-template-columns:auto 32px;"> 
-                    <div>{{key|safe}}</div>
-                    <div align="center" class="glyph" >
+                <div class="items">
+                    <div class="bold active" style="margin:4px; height:28px; display:grid; gap:2px; grid-template-columns:auto 32px;"> 
+                        <div>{{key|safe}}</div>
+                        <div align="center" class="glyph" >
+                            
+                            <i class="fas fa-angle-down"></i>
+                        </div>
+                    </div>                    
+                    <div class="item-group border border-round">
                         
                         
-                        <i class="fas fa-angle-down"></i>
+                        {% for item in sections[key] %}
+                            <div class="item small active" onclick="plot('{{key}}',{{loop.index-1}})">{{item.id}}</div>
+                        {% endfor %}
                     </div>
                     </div>
-                </div>                    
-                <div class="item-group border">
-                    
-                    {% for item in sections[key] %}
-                        <div class="item small active" onclick="plot('{{key}}',{{loop.index-1}})">{{item.id}}</div>
-                    {% endfor %}
                 </div>
                 </div>
-            </div>
-        {% endfor %}
+            {% endfor %}
            </div>
            </div>
             
             
-            <div style="display:grid; align-items:flex-end">
-                <div class="logs border" style="height:250px"></div>
+            <div style="display:none; align-items:flex-end">
+                <div class="logs chart" style=" align-items:center; display:grid" align="center"></div>
 
 
             </div>
             </div>
             
             
         </div>
         </div>
-        <div class="dashboard"> 
+       <div>
+            <div class="dashboard" style="display:none"></div>
             
             
-        </div>
+            {%include 'setup.html' %}
+            
+       </div>
             
             
     </div>
     </div>
     <div class="footer small">
     <div class="footer small">
-        &copy; Vanderbilt University Medical Center
+        Healthcare/IO :: Parser &copy; <span class="year">    </span>
     </div>
     </div>
 </body>
 </body>

+ 391 - 0
healthcareio/server/templates/setup.html

@@ -0,0 +1,391 @@
+<style>
+    .system {height:99%; overflow:hidden;}
+    .data-info .board{ height:300px; display:grid; grid-template-columns:auto 200px 200px; gap:20px; align-items:center}
+    /*.board { background-image: linear-gradient(to bottom, #ffffff,#ffffff,#f3f3f3,#d3d3d3d3)}*/
+    .number {font-size:48px; font-family:courier;padding:8px; ;}
+    .etl {display:grid; grid-template-columns: 250px auto; gap:2;}
+    .chart {box-shadow : 0px 1px 4px 2px #d3d3d3; width:200px; height:250px;
+        display:grid; align-items:center;
+        background-image: linear-gradient(to bottom,#f3f3f3,#ffffff);
+        overflow:hidden;
+        
+        
+    }
+    
+    .dialog { width:450px; min-height:200px; display:grid; grid-template-rows: 40px 80% auto; gap:4px}
+    .dialog .title-bar { border-top-left-radius: 8px; border-top-right-radius: 8px ; padding:4px; background-color:#f3f3f3; gap:2px; display:grid; grid-template-columns: auto 32px; align-items:center}
+    .dialog .action {display:grid; align-items: flex-end; padding-left:25%; padding-right:25%;}
+    .dialog .message {display:grid; align-items: center; grid-template-columns: 20% auto;}
+    .dialog .message .text {line-height:2; text-transform: capitalize;}
+    .fa-exclamation-triangle {color:orange}
+    .fa-question-circle{color:#009df7}
+</style>
+
+<script src="https://cdn.socket.io/socket.io-1.3.5.js"></script>
+<script src="{{context}}/static/js/io/dialog.js"></script>
+<script src="{{context}}/static/js/io/io.js"></script>
+<script src="{{context}}/static/js/io/healthcare.js"></script>
+
+<script>
+    var select = function(node){
+        var value = $($(node).children()[0]).attr('data-value')
+        
+        jx.utils.patterns.visitor($('.tab'),function(_item){
+            var button = $(_item).children()[0]
+            $(_item).removeClass('selected')
+            //alert([$(button).attr('data-value'),value])
+            if($(button).attr('data-value') == value){
+                $(node).addClass('selected')
+                $('.'+value).show()
+                
+            }else{
+                var m = '.'+ $(button).attr('data-value')
+                
+                    $(m).hide()
+                
+                
+            }
+        })
+        
+    }
+    var monitor = {}
+    monitor.listen = {handler:null}
+    
+    monitor.data = function(){
+        var http = HttpClient.instance()
+        http.get("/data",function(x){
+            var r = JSON.parse(x.responseText)
+            var keys = jx.utils.keys(r) //-- process,files
+            for (var i in keys){                
+                var prefix = keys[i]
+                if(prefix == 'process'){
+                    if(r[prefix].counts != 0){
+                        //
+                        // We should insure the listeners are enabled
+                        if(monitor.listen.handler == null){
+                            monitor.listen.handler = setInterval(
+                                function(){
+                                    console.log('running ...')
+                                    monitor.data()},5000)
+
+                        }
+                    }else{
+                        if (monitor.listen.handler != null){
+                            
+                            clearInterval(monitor.listen.handler)
+                        }
+                        dialog.close()
+                    }
+                }
+                monitor.render(prefix,r[prefix])
+            }            
+        })
+    }
+    monitor.render = function(prefix,r){
+        prefix = '.'+prefix
+        
+        var div = jx.dom.get.instance('DIV')
+        var label = jx.dom.get.instance('DIV')
+        div.align = 'center'
+        
+        div.innerHTML = r.counts
+        div.className = 'number'
+        label.innerHTML = prefix.replace(/\./,'')
+        label.style.textTransform = 'capitalize'
+        label.className = 'small bold border-top'
+        div.append(label)
+        
+
+            $(prefix + ' .board').empty()
+            $(prefix+' .board').append(div)
+        
+        var charts = jx.utils.patterns.visitor(r.chart,function(option){
+            
+            
+                var div = jx.dom.get.instance('div')
+            
+                div.className = 'chart'
+                div.align='center'
+                $(prefix+' .board').append(div)
+                
+                var chart = new ApexCharts($(div)[0],option)
+                //chart.render()
+                div.chart = chart
+    
+            return chart
+
+        })  
+        var observers = jx.utils.patterns.visitor(charts,function(_item){
+            var m = function(_chart){
+                this.chart = _chart ;
+                this.apply = function(caller){this.chart.render();                     
+                    caller.notify()
+                }
+            }
+            return new m(_item)
+        })
+        jx.utils.patterns.observer(observers,'apply')
+        //jx.utils.patterns.iterator(charts,'render')
+        
+         /*setTimeout(function(){
+             jx.utils.patterns.visitor(charts,function(_item){_item.render()})
+         },1000)  */ 
+    }
+    var setup = {}
+    setup.open = function(){
+        $('.dashboard').slideUp(
+            function(){
+                $('.setup').slideDown()
+            }
+        )
+        
+    }
+
+    $(document).ready(function(){
+        /*var shandler = new io();
+
+        if (shandler.disconnected ==false){
+            shandler.disconnect()
+        }
+        var socket = io.connect()
+        socket.on('connect',function(e){
+            socket.emit('connect',{name:'steve'})
+        })
+        socket.on('update',function(e){
+            console.log(e)
+            console.log()
+        })
+       var socket = io.connect('http://localhost:81',{cors:{AccessControlAllowOrigin:'*'}}) //('http://localhost:81/stream')
+        socket.on('procs',function(e){
+            
+        })
+        socket.on('data',function(e){
+            
+            $('.logs').empty()
+            var div = $('.logs')
+            
+            var option = e.apex
+            option.plotOptions.pie.size = 220 
+            option.plotOptions.pie = {dataLabels: {show:true,name:{show:true},value:{show:true}}}
+            
+            option.legend.show = false
+            console.log(option)
+            c = new ApexCharts(div[0],option)
+            c.render()
+            socket.emit("procs",{"name":"steve"})
+        })*/
+    
+        select($('.tab')[0])
+        monitor.data()
+        
+        $('.email').text($('#email').val())
+    })
+</script>
+
+    
+    <div class="system setup">
+        <div class="status">
+            
+            {% if not store.type %}
+               <div >
+                   <span class="caption bold border-bottom" style="padding-right:10">Current Configuration</span>
+                   <p></p>
+                   <div style="display:grid; align-items:center; grid-template-columns:32px auto;">
+                       <i class="fa fa-times" style="font-size:28; margin:4px;"></i> <span>System needs to be initialized !</span>
+                   </div>
+                   <p></p>
+                   <div class="active-button border-round" style="width:50%">
+                        <div class="icon">
+                            <i class="fas fa-cog"  style="font-size:28"></i>
+                        </div>
+                        <div class="bold" align="center">Initialize</div>
+                    </div>
+               </div>
+            {% else %}
+                <div class="border-right">
+                    
+                    <span class="caption bold border-bottom" style="padding-right:10">Current Configuration</span>                        
+                    <p>
+                    </p>
+                    <div class="item" style="display:grid; align-items:center">
+                        <div class="bold" style="text-transform: capitalize;">Owner </div><div class="bold">:</div>
+                        <div>
+                            
+                            <input type="text" id="email" value="{{owner}}">
+                        </div>
+                    </div>
+
+                    <div class="item">
+                        <div class="bold" style="text-transform: capitalize;">store</div><div class="bold">:</div>
+                        <div class="">{{store.type}}</div>
+                    </div>
+                    <p></p>
+                    <div class="active-button border-round" style="width:50%" onclick="healthcare.io.update()">
+                        <div class="icon">
+                            <I class="fas fa-download" style="font-size:28"></I>
+                        </div>
+                        <div class="bold" align="center">Update Config</div>
+                    </div>
+                    <p>
+                        <div class="code">
+                            #<br>
+                            healthcare-io.py --init <span class="email"></span> --store mongo
+                        </div>
+                    </p>
+               </div>
+               
+            {%endif%}
+            <p></p>
+            <br>
+            <div class="border-right">
+                <span class="caption bold border-bottom" style="padding-right:10">Manage Plan</span>
+                <p></p>
+                <div style="line-height: 2;">Insure your account is tied to a cloud service provider. 
+                    <br>We support <span class="bold">google-drive, dropbox, one-drive or box. </span>
+                </div>
+                <p>
+                    <div class="bold active-button border-round" style="width:50%" onclick="jx.modal.show({url:'https://healthcareio.the-phi.com/store/healthcareio/plans'})">
+                        <div>
+                            <img src="{{context}}/static/img/logo.svg" />
+                        </div>
+                        <div align="center">Open Plan Console</div>
+                    </div>
+                    </p>
+                
+            </div>
+            <br>
+            <div class="border-right" style="height:30%"></div>
+        </div>
+        <div class="_border-right"></div>
+        <div >
+            <span class="caption bold border-bottom" style="padding-right:10">
+                Manage Processes</span>
+            <p>
+                <div class="input-form" style="grid-template-columns: 30% auto;">
+                    <div class="item" style="grid-row:1; grid-column:1; ">
+                        <div class="label">Process #</div>
+                        <input type="text" class="procs batch"placeholder="#" style="width:64px; text-align:right" value="{{args.batch}}" onchange="healthcare.io.apply()"/>
+                    </div>
+                    <div class="item" style="grid-row:1; grid-column:2 ">
+                        <div class="label">Folder #</div>
+                        <input type="text" placeholder="Process counts" value="/data"/ class="data folder" disabled>
+                    </div>                                                         
+                    
+                </div>
+                <div style="display:grid; grid-template-columns:repeat(2,215px); gap:2px;">
+                    <div class="active-button border-round bold io-apply" style="margin-top:32; display:none" onclick="healthcare.io.apply()">
+                        <div class="icon"><i class="far fa-save" style="font-size:28; color:#4682B4"></i></div>
+                        <div align="center">Apply</div>
+                    </div>
+                    <div class="active-button border-round bold" style="margin-top:32" onclick="healthcare.io.stop()">
+                        <i class="far fa-stop-circle" style="font-size:28; color:maroon"></i>
+                        
+                        <div align="center">Stop</div>
+                    </div>
+                    <div class="active-button border-round bold" style="margin-top:32" onclick="healthcare.io.run()">
+                        <i class="fas fa-running" style="font-size:28; color:green"></i>
+                        <div align="center">Run</div>
+                    </div>
+
+                </div>                   
+               
+            </p>
+            <p>
+                <div class="code">
+                    <div class="bold"># The command that will be executed</div>
+                    <div>healthcare-io.py --parse --folder /data --batch <span class="batch">{{args.batch}}</span></div>
+                </div>
+            </p>
+            <p>
+                <div style="display:grid; grid-template-columns:auto 48px ; gap:2px">
+                    <div class="bold caption border-bottom">Process Monitoring</div>
+                    <div class="active" align="center" title="reload" onclick="monitor.data()"><i class="fas fa-sync"></i></div>
+                </div>
+                <div class="small">Powered by smart-top</div>
+                <p></p>
+                <div class="tabs">
+                    <div class="tab selected" onclick="select(this)">
+                        <div class="active" data-value="process" >
+                            <i class="far fa-clock" style="color:maroon"></i>
+                            <span>Process</span>
+                        </div>
+                    </div>
+                    <div class="tab" onclick="select(this)">
+                        <div class="active" data-value="files"><i class="fas fa-file-alt"></i> Files</div>
+                    </div>
+                    <div class="tab" onclick="select(this)">
+                        <div class="active" data-value="export"><i class="fas fa-upload"></i> Export</div>
+                    </div>
+                    
+                </div>
+                <div class="data-info">
+                    <div class="process ">
+                        
+                        <div class="board"></div>
+                       <div class="small" align="center">
+                            <div class="border-top bold" style="color:#4682B4;">Running Processes and resource usage</div>
+                       </div>
+                    </div>
+                    <div class="files">
+                        <div class="board"></div>
+                        <div class="small" align="center">
+                            <div class="border-top bold" style="color:#4682B4;">Summary of files found and processed</div>
+                       </div>
+
+                    </div>
+                    <div class="export">
+                        <p></p>                        
+                        <div class="etl">
+                            <div class="" >
+                                <div class="menu" style="position:absolute; width:200">
+                                    <div class="items ">
+                                        <div class="bold active" style="display:grid; grid-template-columns:80% auto;">
+                                            <span>
+                                                <i class="fas fa-cloud"></i>
+                                                Cloud</span>
+                                            <span class="glyph">
+                                                <i class="fas fa-angle-down"></i>
+                                            </span>
+                                        </div>
+                                        <div class="item-group border-round border small">
+                                            <div class="item" onclick="healthcare.io.publish.database.init('s3')">AWS S3</div>
+                                            <div class="item" onclick="healthcare.io.publish.database.init('bigquery')">Google Bigquery</div>
+                                            
+                                        </div>
+                                    </div>
+                                   <div class="items ">
+                                       <div class="bold active"style="display:grid; grid-template-columns:80% auto;">
+                                            <span>
+                                                <i class="fas fa-database"></i>
+                                                Database</span>
+                                            <span class="glyph">
+                                                <i class="fas fa-angle-down"></i>
+                                            </span>
+                                        </div>
+                                       <div class="item-group border-round border small">
+                                           <div class="bold">SQL</div>
+                                           
+                                                <div class="item" style="margin-left:15px; margin-right:32px" onclick="healthcare.io.publish.database.init('postgresql')">PostgreSQL</div>
+                                                <div class="item" style="margin-left:15px; margin-right:32px" onclick="healthcare.io.publish.database.init('mysql')">MySQL</div>
+                                                
+         
+                                           
+                                           <div class="bold">NoSQL</div>
+                                            <div class="item" style="margin-left:15px; margin-right:32px" onclick="healthcare.io.publish.database.init('mongodb')">Mongodb</div>
+                                            <div class="item" style="margin-left:15px; margin-right:32px" onclick="healthcare.io.publish.database.init('couchdb')">Couchdb</div>
+                                       </div>
+                                   </div>
+                                </div>
+                            </div>
+                            <div>
+                                <div class="active-button border-round" style="width:50%">
+                                    <div class="icon"><i class="fas fa-running" style="font-size:28"></i></div> <div class="bold" align="center">Start</div>
+                                </div>
+                            </div>
+                        </div>
+
+                    </div>
+                </div>
+            </p>
+        </div>
+    </div>

+ 78 - 0
healthcareio/server/templates/store.html

@@ -0,0 +1,78 @@
+<link href="{{context}}/static/css/fa/css/all.css" type="text/css" rel="stylesheet">
+<link rel="stylesheet" href="{{context}}/static/css/default.css" type="text/css">
+<link href="{{context}}/static/css/borders.css" type="text/css" rel="stylesheet">
+<style>
+    .form , input{font-family:sans-serif; font-size:18px}
+    .form .small {font-size:14px; line-height:2}
+    .mongo, .couchdb{
+    
+        display:grid;
+        gap:2px;
+    }
+    .grid-full {display:grid; grid-template-columns: 100%;}
+    .grid-split-half {display:grid; grid-template-columns: 50% 50%; gap:2px;}
+    .store .title-bar {display:grid; align-items:center; grid-template-columns: auto 32px; padding:8px;}
+    .file {display:grid; align-items:center; grid-template-columns: 40% auto; gap:2px; font-family:sans-serif; padding:8px}
+    .file input {display:none}
+    .file label {padding:8px;}
+    
+    
+</style>
+<div class="store">
+    
+    <div class="title-bar">
+        <div class="caption">Export Module <span class="bold id"></span></div>
+        <div class="active" align="center" onclick="jx.modal.close()">
+            <i class="fas fa-times"></i>
+        </div>
+    </div>
+    <p></p>
+
+    <div class="cloud form border-round">
+        <div class="small border-round">
+            Please provide select access-key file or service account key file to perform ETL            
+            to <span class="bold id"></span>. <br>The files will be re-created in JSON format
+        </div>
+        <div class="file">
+            <label class="active-button border-round">
+                <span style="font-family:sans-serif">
+                    <i class="icon fas fa-file-upload" style="font-size:24px"></i>
+                </span>               
+                <div>Select key file</div> 
+                <input type="file" id="file" aria-label="File browser example" onchange="publish.set.file()">
+
+            </label>
+            <div class="name small black bold" style="padding-left:24px"></div>
+        </div>
+    </div>
+   <div class="database form border-round">
+       <div class="small border-round">
+        Tables / collections will be automatically inferred in the <span class="bold id"></span>
+       </div>
+        <div class="mongo">
+
+            <div class="grid-split-half">
+                <input type="text" class="host" placeholder="host:port"/>
+                <input type="text" class="host" placeholder="database"/>
+            </div>
+           
+            <div class="grid-split-half">
+                <input type="text" placeholder="user"/>
+            
+                <input type="text" placeholder="password"/>
+            </div>
+
+        </div>
+        <p></p>
+        <div>
+            <br>
+            <div class="active-button border-round" style="margin-left:30%; margin-right:30%">
+                <div class="icon">
+                    <i class="fas fa-check"></i>
+                </div>
+                <div class="bold" align="center">Export Now</div>
+            </div>
+        </div>
+    
+   </div>
+</div>

+ 100 - 23
healthcareio/x12/__init__.py

@@ -24,6 +24,7 @@ import sys
 from itertools import islice
 from itertools import islice
 from multiprocessing import Process
 from multiprocessing import Process
 import transport
 import transport
+import jsonmerge
 class void :
 class void :
     pass
     pass
 class Formatters :
 class Formatters :
@@ -49,8 +50,9 @@ class Formatters :
         """
         """
         This function is designed to split an x12 row and 
         This function is designed to split an x12 row and 
         """
         """
+        value = []
         if row.startswith(prefix) is False:
         if row.startswith(prefix) is False:
-            value = []
+            
             
             
             for row_value in row.replace('~','').split(sep) :
             for row_value in row.replace('~','').split(sep) :
                 
                 
@@ -65,10 +67,12 @@ class Formatters :
                 else :
                 else :
                     
                     
                     value.append(row_value.replace('\n',''))
                     value.append(row_value.replace('\n',''))
-            return [xchar.replace('\r','') for xchar in value] #row.replace('~','').split(sep)
+            value =  [xchar.replace('\r','') for xchar in value] #row.replace('~','').split(sep)
         else:
         else:
             
             
-            return [ [prefix]+ self.split(item,'>') for item in row.replace('~','').split(sep)[1:] ]
+            value =  [ [prefix]+ self.split(item,'>') for item in row.replace('~','').split(sep)[1:] ]
+        
+        return value if type(value) == list and type(value[0]) != list else value[0]
     def get_config(self,config,row):
     def get_config(self,config,row):
         """
         """
         This function will return the meaningfull parts of the configuration for a given item
         This function will return the meaningfull parts of the configuration for a given item
@@ -92,7 +96,6 @@ class Formatters :
             if _row[0] in config['SIMILAR']    :
             if _row[0] in config['SIMILAR']    :
                 key = config['SIMILAR'][_row[0]]
                 key = config['SIMILAR'][_row[0]]
                 _info = config[key]
                 _info = config[key]
-        
         return _info
         return _info
     
     
     def hash(self,value):
     def hash(self,value):
@@ -130,7 +133,7 @@ class Formatters :
             terms = value[1].split('>')
             terms = value[1].split('>')
             return {'type':terms[0],'code':terms[1],"amount":float(value[2])}
             return {'type':terms[0],'code':terms[1],"amount":float(value[2])}
         else:
         else:
-            
+           
             return {"code":value[2],"type":value[1],"amount":float(value[3])}
             return {"code":value[2],"type":value[1],"amount":float(value[3])}
     def sv2(self,value):
     def sv2(self,value):
         #
         #
@@ -170,27 +173,56 @@ class Formatters :
     def pos(self,value):
     def pos(self,value):
         """
         """
             formatting place of service information within a segment (REF)
             formatting place of service information within a segment (REF)
+            @TODO: In order to accomodate the other elements they need to be specified in the configuration
+                Otherwise it causes problems on export
         """
         """
         
         
         xchar = '>' if '>' in value else ':'
         xchar = '>' if '>' in value else ':'
         x = value.split(xchar)    
         x = value.split(xchar)    
         x =  {"code":x[0],"indicator":x[1],"frequency":x[2]} if len(x) == 3 else {"code":x[0],"indicator":None,"frequency":None}
         x =  {"code":x[0],"indicator":x[1],"frequency":x[2]} if len(x) == 3 else {"code":x[0],"indicator":None,"frequency":None}
-        return x
+        return x['code']
 class Parser (Process):
 class Parser (Process):
     def __init__(self,path):
     def __init__(self,path):
+        """
+            :path       path of the configuration file (it can be absolute)
+        """
         Process.__init__(self)
         Process.__init__(self)
         self.utils  = Formatters()
         self.utils  = Formatters()
         self.get    = void()
         self.get    = void()
         self.get.value = self.get_map
         self.get.value = self.get_map
         self.get.default_value = self.get_default_value
         self.get.default_value = self.get_default_value
         _config = json.loads(open(path).read())
         _config = json.loads(open(path).read())
-        
+        self._custom_config = self.get_custom(path)
         self.config = _config['parser']
         self.config = _config['parser']
         self.store  = _config['store']        
         self.store  = _config['store']        
         
         
         self.files = []
         self.files = []
         self.set = void()
         self.set = void()
         self.set.files = self.set_files
         self.set.files = self.set_files
+        self.emit = void()
+        self.emit.pre =  None
+        self.emit.post = None
+    def get_custom(self,path) :
+        """
+        :path   path of the configuration file (it can be absolute)
+        """
+        #
+        #
+        _path = path.replace('config.json','')
+        if _path.endswith(os.sep) :
+            _path = _path[:-1]
+        
+        _config = {}
+        _path = os.sep.join([_path,'custom'])
+        if os.path.exists(_path) :
+            
+            files = os.listdir(_path)
+            if files :
+                fullname = os.sep.join([_path,files[0]])
+                
+                _config = json.loads ( (open(fullname)).read() )
+        return _config
+
     def set_files(self,files):
     def set_files(self,files):
         self.files = files
         self.files = files
     def get_map(self,row,config,version=None):
     def get_map(self,row,config,version=None):
@@ -241,15 +273,18 @@ class Parser (Process):
                         
                         
                         value = {key:value} if key not  in value else value
                         value = {key:value} if key not  in value else value
                         
                         
+                        
                     else:
                     else:
                         if 'syn' in config and value in config['syn'] :
                         if 'syn' in config and value in config['syn'] :
                             value = config['syn'][value]
                             value = config['syn'][value]
                     if type(value) == dict :
                     if type(value) == dict :
                         
                         
                         object_value = dict(object_value, **value) 
                         object_value = dict(object_value, **value) 
+                        
                     else:
                     else:
                         
                         
                         object_value[key] = value
                         object_value[key] = value
+                        
         else:
         else:
             #
             #
             # we are dealing with a complex object
             # we are dealing with a complex object
@@ -269,25 +304,35 @@ class Parser (Process):
         return object_value
         return object_value
     def apply(self,content,_code) :
     def apply(self,content,_code) :
         """
         """
-            :file content i.e a segment with the envelope
-            :_code  837 or 835 (helps get the appropriate configuration)
+        :content    content of a file i.e a segment with the envelope
+        :_code  837 or 835 (helps get the appropriate configuration)
         """
         """
         util   = Formatters()
         util   = Formatters()
         # header       = default_value.copy()
         # header       = default_value.copy()
         value = {}
         value = {}
+        
         for row in content[:] :
         for row in content[:] :
             
             
+            
             row     = util.split(row.replace('\n','').replace('~',''))
             row     = util.split(row.replace('\n','').replace('~',''))
             _info   = util.get.config(self.config[_code][0],row)
             _info   = util.get.config(self.config[_code][0],row)
-          
+            if self._custom_config and _code in self._custom_config:
+                _cinfo   = util.get.config(self._custom_config[_code],row)
+            else:
+                _cinfo = {}
+            # _info   = self.consolidate(row=row,type=_code,config=_info,util=util)
+            # print ([row[0],_info])
+            # print ()
+            # continue
+            # _cinfo   = util.get.config(self._custom_config[_code],row)
+            
+            
             if _info :
             if _info :
+
                 try:
                 try:
-                    
+                    _info = jsonmerge.merge(_info,_cinfo)
                     tmp = self.get.value(row,_info)
                     tmp = self.get.value(row,_info)
-                    # if 'P1080351470' in content[0] and 'PLB' in row:
-                    #     print (_info)
-                    #     print (row)
-                    #     print (tmp)
+                    
                     if not tmp :
                     if not tmp :
                         continue 
                         continue 
                     if 'label' in _info :
                     if 'label' in _info :
@@ -300,10 +345,10 @@ class Parser (Process):
                         else:
                         else:
                             if label not in value:   
                             if label not in value:   
                                 value[label] = [tmp]
                                 value[label] = [tmp]
-                            elif len(list(tmp.keys())) == 1 :
-                                # print "\t",len(claim[label]),tmp
-                                index = len(value[label]) -1 
-                                value[label][index] = dict(value[label][index],**tmp)
+                            # elif len(list(tmp.keys())) == 1 :
+                            #     # print "\t",len(claim[label]),tmp
+                            #     index = len(value[label]) -1 
+                            #     value[label][index] = dict(value[label][index],**tmp)
                             else:
                             else:
                                 value[label].append(tmp)                        
                                 value[label].append(tmp)                        
                         tmp['_index'] = len(value[label]) -1 
                         tmp['_index'] = len(value[label]) -1 
@@ -319,7 +364,9 @@ class Parser (Process):
                     elif 'field' in _info :
                     elif 'field' in _info :
                         
                         
                         name = _info['field']
                         name = _info['field']
-                        value[name] = tmp
+                        # value[name] = tmp
+                        value = jsonmerge.merge(value,{name:tmp})
+                      
                     else:
                     else:
                         
                         
 
 
@@ -327,13 +374,14 @@ class Parser (Process):
                     
                     
                     pass
                     pass
                 except Exception as e :
                 except Exception as e :
-                   
+                    
                     print ('__',e.args)
                     print ('__',e.args)
                     pass
                     pass
                 
                 
         return value if value else {}
         return value if value else {}
 
 
     def get_default_value(self,content,_code):
     def get_default_value(self,content,_code):
+        
         util = Formatters()
         util = Formatters()
         TOP_ROW = content[1].split('*')
         TOP_ROW = content[1].split('*')
         CATEGORY= content[2].split('*')[1].strip()
         CATEGORY= content[2].split('*')[1].strip()
@@ -352,6 +400,8 @@ class Parser (Process):
             value['payer_id'] = SENDER_ID               
             value['payer_id'] = SENDER_ID               
         else:
         else:
             value['provider_id'] = SENDER_ID
             value['provider_id'] = SENDER_ID
+        #
+        # Let's parse this for default values            
         return value
         return value
 
 
     def read(self,filename) :
     def read(self,filename) :
@@ -374,8 +424,14 @@ class Parser (Process):
                 INITIAL_ROWS = file[:4]
                 INITIAL_ROWS = file[:4]
             if len(INITIAL_ROWS) < 3 :
             if len(INITIAL_ROWS) < 3 :
                 return None,[{"name":filename,"completed":False}],None
                 return None,[{"name":filename,"completed":False}],None
-            section = 'HL' if INITIAL_ROWS[1].split('*')[1] == 'HC' else 'CLP'            
-            _code   = '837' if section == 'HL' else '835'
+            # section = 'HL' if INITIAL_ROWS[1].split('*')[1] == 'HC' else 'CLP'       
+            # _code   = '837' if section == 'HL' else '835'
+            # print ([_code,section])
+            _code = INITIAL_ROWS[2].split('*')[1].strip()
+            # section = 'CLP' if _code == '835' else 'HL'
+            section  = self.config[_code][0]['SECTION'].strip()
+            #
+            # adjusting the 
             DEFAULT_VALUE = self.get.default_value(INITIAL_ROWS,_code)
             DEFAULT_VALUE = self.get.default_value(INITIAL_ROWS,_code)
             DEFAULT_VALUE['name'] = filename.strip()
             DEFAULT_VALUE['name'] = filename.strip()
             #
             #
@@ -383,22 +439,30 @@ class Parser (Process):
             #   index 1 identifies file type i.e CLM for claim and CLP for remittance
             #   index 1 identifies file type i.e CLM for claim and CLP for remittance
             segment = []
             segment = []
             index = 0;
             index = 0;
+            _toprows = []
             for row in file :
             for row in file :
+                row = row.replace('\r','')
+                if not segment and not row.startswith(section):
+                    _toprows += [row]
                 if row.startswith(section) and not segment:
                 if row.startswith(section) and not segment:
                     
                     
                     segment = [row]
                     segment = [row]
+
                     continue
                     continue
                     
                     
                 elif segment and not row.startswith(section):
                 elif segment and not row.startswith(section):
                     
                     
                     segment.append(row)
                     segment.append(row)
+                
                     
                     
                 if len(segment) > 1 and row.startswith(section):
                 if len(segment) > 1 and row.startswith(section):
                     #
                     #
                     # process the segment somewhere (create a thread maybe?)
                     # process the segment somewhere (create a thread maybe?)
                     # 
                     # 
                     # default_claim = dict({"index":index},**DEFAULT_VALUE)
                     # default_claim = dict({"index":index},**DEFAULT_VALUE)
+                    # print (_toprows)
                     _claim = self.apply(segment,_code)
                     _claim = self.apply(segment,_code)
+                    
                     # if _claim['claim_id'] == 'P1080351470' :
                     # if _claim['claim_id'] == 'P1080351470' :
                     #     print (_claim)
                     #     print (_claim)
                         # _claim = dict(DEFAULT_VALUE,**_claim)
                         # _claim = dict(DEFAULT_VALUE,**_claim)
@@ -418,12 +482,14 @@ class Parser (Process):
                 claim = self.apply(segment,_code)
                 claim = self.apply(segment,_code)
                 if claim :
                 if claim :
                     claim['index'] = len(claims)
                     claim['index'] = len(claims)
+                    claim = jsonmerge.merge(claim,self.apply(_toprows,_code))
                     claims.append(dict(DEFAULT_VALUE,**claim))
                     claims.append(dict(DEFAULT_VALUE,**claim))
             if type(file) != list :
             if type(file) != list :
                 file.close()
                 file.close()
 
 
             # x12_file = open(filename.strip(),errors='ignore').read().split('\n')
             # x12_file = open(filename.strip(),errors='ignore').read().split('\n')
         except Exception as e:
         except Exception as e:
+           
             logs.append ({"parse":_code,"completed":False,"name":filename,"msg":e.args[0]})
             logs.append ({"parse":_code,"completed":False,"name":filename,"msg":e.args[0]})
             return [],logs,None
             return [],logs,None
         
         
@@ -432,6 +498,9 @@ class Parser (Process):
         # self.finish(claims,logs,_code)
         # self.finish(claims,logs,_code)
         return claims,logs,_code    
         return claims,logs,_code    
     def run(self):
     def run(self):
+        if self.emit.pre :
+            self.emit.pre()
+
         for filename in self.files :
         for filename in self.files :
             content,logs,_code = self.read(filename)
             content,logs,_code = self.read(filename)
             self.finish(content,logs,_code)
             self.finish(content,logs,_code)
@@ -441,14 +510,22 @@ class Parser (Process):
         if args['type'] == 'mongo.MongoWriter' :
         if args['type'] == 'mongo.MongoWriter' :
             args['args']['doc'] = 'claims' if _code == '837' else 'remits'
             args['args']['doc'] = 'claims' if _code == '837' else 'remits'
             _args['args']['doc'] = 'logs'
             _args['args']['doc'] = 'logs'
+        else:
+            args['args']['table'] = 'claims' if _code == '837' else 'remits'
+            _args['args']['table'] = 'logs'
+
         if content      :
         if content      :
             writer = transport.factory.instance(**args)
             writer = transport.factory.instance(**args)
             writer.write(content)
             writer.write(content)
             writer.close()
             writer.close()
         if logs :
         if logs :
+            
             logger = transport.factory.instance(**_args)
             logger = transport.factory.instance(**_args)
             logger.write(logs)
             logger.write(logs)
+            
             logger.close()
             logger.close()
+        if self.emit.post :
+            self.emit.post(content,logs)
 
 
 
 
 
 

+ 3 - 2
setup.py

@@ -8,14 +8,15 @@ import sys
 def read(fname):
 def read(fname):
     return open(os.path.join(os.path.dirname(__file__), fname)).read() 
     return open(os.path.join(os.path.dirname(__file__), fname)).read() 
 args = {
 args = {
-    "name":"healthcareio","version":"1.3.7",
+    "name":"healthcareio","version":"1.4.8",
     "author":"Vanderbilt University Medical Center",
     "author":"Vanderbilt University Medical Center",
     "author_email":"steve.l.nyemba@vumc.org",
     "author_email":"steve.l.nyemba@vumc.org",
+    "include_package_data":True,
     "license":"MIT",
     "license":"MIT",
     "packages":find_packages(),
     "packages":find_packages(),
     "keywords":["healthcare","edi","x12","analytics","835","837","data","transport","protocol"]
     "keywords":["healthcare","edi","x12","analytics","835","837","data","transport","protocol"]
 }
 }
-args["install_requires"] = ['seaborn','jinja2', 'weasyprint','data-transport@git+https://dev.the-phi.com/git/steve/data-transport.git','pymongo','numpy','cloudant','pika','boto','flask-session','smart_open']
+args["install_requires"] = ['flask-socketio','seaborn','jinja2','jsonmerge', 'weasyprint','data-transport@git+https://healthcareio.the-phi.com/git/code/transport.git','pymongo','numpy','cloudant','pika','boto','botocore','flask-session','smart_open','smart-top@git+https://healthcareio.the-phi.com/git/code/smart-top.git@data-collector']
 args['url'] = 'https://hiplab.mc.vanderbilt.edu'
 args['url'] = 'https://hiplab.mc.vanderbilt.edu'
 args['scripts']= ['healthcareio/healthcare-io.py']
 args['scripts']= ['healthcareio/healthcare-io.py']
 # args['entry_points'] = {
 # args['entry_points'] = {