Package Gnumed :: Module gnumed
[frames] | no frames]

Source Code for Module Gnumed.gnumed

   1  #!/usr/bin/env python3 
   2   
   3  __doc__ = MANPAGE = """.\\" ======================================================== 
   4  .\\" SPDX-License-Identifier: GPL-2.0-or-later 
   5  .\\" ======================================================== 
   6   
   7  .TH GNUmed 1 "%s" "Manual for GNUmed" 
   8   
   9  .SH NAME 
  10  .B GNUmed 
  11  - an electronic MEDICAL record software for GP offices 
  12   
  13  This is not fully featured yet. Use at your own risk. 
  14  You have been warned. 
  15   
  16  .SH SYNOPSIS 
  17  .B gnumed 
  18  .RB [--quiet|debug] 
  19  .RB [--slave] 
  20  .RB [--text-domain=TEXTDOMAIN] 
  21  .RB [--log-file=FILE] 
  22  .RB [--conf-file=FILE] 
  23  .RB [--profile=FILE] 
  24  .RB [--lang-gettext=LANGUAGE] 
  25  .RB [--tool=TOOL] 
  26  .RB [--override-schema-check] 
  27  .RB [--skip-update-check] 
  28  .RB [--local-import] 
  29  .RB [--help] 
  30  .RB [--version] 
  31  .RB [-V] 
  32  .RB [-h|?] 
  33   
  34   
  35  .SH DESCRIPTION 
  36  .B GNUmed 
  37  is a solution for keeping safe and medically sound electronic 
  38  records on a patient's health. It primarily focuses on GP 
  39  offices. It is released under the GPL. 
  40   
  41  GNUmed is written in Python with wxPython/wxWindows. Data is 
  42  stored in a PostgreSQL database. Multiple clients can work 
  43  with the same database at the same time. 
  44   
  45  .SH OPTIONS 
  46  .PP 
  47  .TP 
  48  .B \--quiet 
  49  Be extra quiet and show only _real_ errors in the log. 
  50  .TP 
  51  .B \--debug 
  52  Pre-set the [debug mode] checkbox in the login dialog 
  53  which controls increased verbosity in the log file. 
  54   
  55  Useful for, well, debugging :-)  Slower, too. 
  56  .TP 
  57  .B \--slave 
  58  Pre-set the [enable remote control] checkbox in the login 
  59  dialog to enable the XML-RPC remote control feature. 
  60  .TP 
  61  .B \--hipaa 
  62  Enable HIPAA features with user workflow impact. 
  63   
  64  Those features which do not affect the workflow of the user 
  65  are permanently enabled. 
  66  .TP 
  67  .B \--log-file=FILE 
  68  Use this to change the name of the log file. At startup 
  69  GNUmed will tell you the name of the log file in use. 
  70  .TP 
  71  .B \--conf-file=FILE 
  72  Use configuration file FILE instead of searching for it in standard locations. 
  73  .TP 
  74  .B \--profile=FILE 
  75  Activate profiling and write profile data to file FILE. 
  76  .TP 
  77  .B \--lang-gettext=LANGUAGE 
  78  Explicitly set the language to use in gettext translation. The very 
  79  same effect can be achieved by setting the environment variable $LANG 
  80  from a launcher script. 
  81  .TP 
  82  .B \--text-domain=TEXTDOMAIN 
  83  Set this to change the name of the language file to be loaded. 
  84  Note, this does not change the directory the file is searched in, 
  85  only the name of the file where messages are loaded from. The 
  86  standard textdomain is, of course, "gnumed.mo". You need only 
  87  specify the base name of the file without the .mo extension. 
  88  .TP 
  89  .B \--tool=TOOL 
  90  Run the named TOOL instead of a GUI. 
  91   
  92  Currently implemented tools: 
  93   
  94          check_enc_epi_xref: Cross-check that foreign keys values in any given row of any table carrying both of fk_episode and fk_encounter do point to episodes and encounters, respectively, of the very same patient. 
  95   
  96          export_pat_emr_structure: Export the EMR structure (issues and episodes) of a patient into a text file. 
  97   
  98          check_mimetypes_in_archive: Show mimetypes and related information of all document parts in the archive. 
  99   
 100          read_all_rows_of_table: Check readability of all rows of a given table. 
 101  .TP 
 102  .B \--override-schema-check 
 103  Continue loading the client even if the database schema 
 104  version and the client software version cannot be verified 
 105  to be compatible. 
 106  .TP 
 107  .B \--skip-update-check 
 108  Skip checking for client updates. This is useful during 
 109  development or when the update check URL is unavailable. 
 110  .TP 
 111  .B \--local-import 
 112  At startup adjust the PYTHONPATH such that the GNUmed client is 
 113  run from a local copy of the source tree (say an unpacked tarball 
 114  or a GIT repo) rather than from a proper system-wide installation. 
 115  .TP 
 116  .B \--version, -V 
 117  Show version information about the GNUmed client and the 
 118  database it needs. 
 119  .TP 
 120  .B \--help, -h, or -? 
 121  Show this help. 
 122   
 123   
 124  .SH CONFIGURATION 
 125  .PP 
 126  .TP 
 127  .B Client startup and shutdown (OS level) 
 128   
 129  A shell script /usr/bin/gnumed is used to startup the client. 
 130  It checks whether the systemwide configuration file 
 131   
 132          /etc/gnumed/gnumed-client.conf 
 133   
 134  exists. It then executes the following scripts (in that 
 135  order) if found: 
 136   
 137          /etc/gnumed/gnumed-startup-local.sh 
 138   
 139          ~/.gnumed/scripts/gnumed-startup-local.sh 
 140   
 141  When the client terminates it will execute the following 
 142  scripts in order if they exist: 
 143   
 144          /etc/gnumed/gnumed-shutdown-local.sh 
 145   
 146          ~/.gnumed/scripts/gnumed-shutdown-local.sh 
 147   
 148  .PP 
 149  .TP 
 150  .B wxPython client startup 
 151   
 152  The gnumed.py script checks for INI style configuration files 
 153  and fails if it does not find any. The files are searched for 
 154  in the following order and extend/overwrite each others 
 155  options: 
 156   
 157          in the current working directory (cwd) 
 158   
 159                  ./gumed.conf 
 160   
 161          in the systemwide configuration directory 
 162   
 163                  /etc/gnumed/gnumed-client.conf 
 164   
 165          in the home directory 
 166   
 167                  ~/.gnumed/gnumed.conf 
 168   
 169          in a local git tree or unpacked tarball 
 170   
 171                  .../gnumed/client/gnumed.conf 
 172   
 173          explicitly given by CLI option 
 174   
 175                  --conf-file=<CONF FILE> 
 176   
 177  .PP 
 178  .TP 
 179  .B client/system interfacing 
 180   
 181  .B ~/.gnumed/gnumed-xsanerc.conf 
 182   
 183  (requires XSane > v0.992) 
 184   
 185  When GNUmed invokes XSane for scanning it passes along this file (via =--xsane-rc=). This way a custom XSane configuration can be used with GNUmed. If the file doesn't exist it will be created from ~/.sane/xsane/xsanerc on the first call to XSane. 
 186   
 187  When you configure XSane after calling it from GNUmed your changes will be stored in the GNUmed-specific XSane configuration file and will not affect your usual XSane settings. 
 188   
 189  .B mime_type2file_extension.conf 
 190   
 191  (searched for in ~/.gnumed/ and /etc/gnumed/, in that order) 
 192   
 193  GNUmed will use these files to map mime types to file extensions if need be. 
 194   
 195  The file must contain a group [extensions] under which there can be one option per mime type specifying the extension to use on files of said type. Set the value to the raw extension only, omitting the ".", like so: 
 196   
 197  .nf 
 198  [extensions] 
 199  image/x-bmp = bmp 
 200   
 201   
 202  .SH EXIT STATUS 
 203  .TP 
 204   > 0: some error occurred while the GUI client was run 
 205  .TP 
 206     0: normal termination of the client 
 207  .TP 
 208   < 0: some error occurred while trying to run a console tool 
 209  .TP 
 210    -1: an unknown console tool was requested 
 211  .TP 
 212  < -1: an error occurred while a console tool was run 
 213  .TP 
 214  -999: hard abort of the client 
 215   
 216   
 217  .SH ENVIRONMENT 
 218  .TP 
 219  .B LANG, LC_MESSAGES, etc. 
 220  See gettext(1) for how the various locale related environment variables work. 
 221   
 222   
 223  .SH OTHER FILES AND DIRECTORIES 
 224  .PP 
 225  .TP 
 226  .B ~/.gnumed/gnumed.log 
 227  The default log file. 
 228  .TP 
 229  .B gnumed-client.tmpfiles.d.conf 
 230  Integration with systemd-tmpfiles(8). 
 231  .TP 
 232  .B gnumed-completion.bash 
 233  Integration with BASH completions. 
 234   
 235   
 236  .SH SEE ALSO 
 237  .PP 
 238  .TP 
 239  .B https://www.gnumed.[de|org] 
 240  Online documenation. 
 241  .TP 
 242  .B http://savannah.gnu.org/projects/gnumed 
 243  Mailing list home 
 244  .TP 
 245  .B https://github.org/ncqgm/gnumed 
 246  Source code repository (Git) 
 247  .TP 
 248  .B /usr/share/doc/gnumed/ 
 249  Local documentation 
 250  .TP 
 251  .B man -k gm-* 
 252  List man pages on gm-* commands. 
 253  .TP 
 254  .B gettext(1) 
 255   
 256   
 257  .SH BUGS 
 258   
 259  A lot of functionality is still missing. To make up for 
 260  that, there's bugs here and there for you to report :-) 
 261   
 262  Use at your own risk. You have been warned. Take proper backups ! 
 263  """ 
 264   
 265  #========================================================== 
 266  # SPDX-License-Identifier: GPL-2.0-or-later 
 267  __author__ = "H. Herb <hherb@gnumed.net>, K. Hilbert <Karsten.Hilbert@gmx.net>, I. Haywood <i.haywood@ugrad.unimelb.edu.au>" 
 268  __license__ = "GPL v2 or later (details at http://www.gnu.org)" 
 269   
 270   
 271  # standard library 
 272  import sys 
 273  import os 
 274  import platform 
 275  import tempfile 
 276  import faulthandler 
 277  import random 
 278  import logging 
 279  import datetime 
 280  import signal 
 281  import os.path 
 282  import shutil 
 283  import stat 
 284  import re as regex 
 285   
 286   
 287  # do not run as module 
 288  if __name__ != "__main__": 
 289          print("GNUmed startup: This is not intended to be imported as a module !") 
 290          print("-----------------------------------------------------------------") 
 291          sys.exit(1) 
 292   
 293   
 294  # do not run as root 
 295  if os.name in ['posix'] and os.geteuid() == 0: 
 296          print(""" 
 297  GNUmed startup: GNUmed should not be run as root. 
 298  ------------------------------------------------- 
 299   
 300  Running GNUmed as <root> can potentially put all 
 301  your medical data at risk. It is strongly advised 
 302  against. Please run GNUmed as a non-root user. 
 303  """) 
 304          sys.exit(1) 
 305   
 306  #---------------------------------------------------------- 
 307  #current_client_version = '1.8.0rc3' 
 308  #current_client_branch = '1.8' 
 309  current_client_version = 'head' 
 310  current_client_branch = 'master' 
 311   
 312   
 313  _log = None 
 314  _pre_log_buffer = [] 
 315  _cfg = None 
 316  _old_sig_term = None 
 317  _known_short_options = 'h?V' 
 318  _known_long_options = [ 
 319          'debug', 
 320          'slave', 
 321          'skip-update-check', 
 322          'profile=', 
 323          'text-domain=', 
 324          'log-file=', 
 325          'conf-file=', 
 326          'lang-gettext=', 
 327          'override-schema-check', 
 328          'local-import', 
 329          'help', 
 330          'version', 
 331          'hipaa', 
 332          'wxp=', 
 333          'tool=', 
 334          'tui' 
 335  ] 
 336   
 337  _known_tools = [ 
 338          'check_enc_epi_xref', 
 339          'export_pat_emr_structure', 
 340          'check_mimetypes_in_archive', 
 341          'read_all_rows_of_table', 
 342          'generate_man_page' 
 343  ] 
 344   
 345   
 346  import_error_sermon = """ 
 347  GNUmed startup: Cannot load GNUmed Python modules ! 
 348  --------------------------------------------------- 
 349  CRITICAL ERROR: Program halted. 
 350   
 351  Please make sure you have: 
 352   
 353   1) the required third-party Python modules installed 
 354   2) the GNUmed Python modules linked or installed into site-packages/ 
 355      (if you do not run from a CVS tree the installer should have taken care of that) 
 356   3) your PYTHONPATH environment variable set up correctly 
 357   
 358  <sys.path> is currently set to: 
 359   
 360   %s 
 361   
 362  If you are running from a copy of the CVS tree make sure you 
 363  did run gnumed/check-prerequisites.sh with good results. 
 364   
 365  If you still encounter errors after checking the above 
 366  requirements please ask on the mailing list. 
 367  """ 
 368   
 369   
 370  missing_cli_config_file = """ 
 371  GNUmed startup: Missing configuration file. 
 372  ------------------------------------------- 
 373   
 374  You explicitly specified a configuration file 
 375  on the command line: 
 376   
 377          --conf-file=%s 
 378   
 379  The file does not exist, however. 
 380  """ 
 381   
 382   
 383  no_config_files = """ 
 384  GNUmed startup: Missing configuration files. 
 385  -------------------------------------------- 
 386   
 387  None of the below candidate configuration 
 388  files could be found: 
 389   
 390   %s 
 391   
 392  Cannot run GNUmed without any of them. 
 393  """ 
 394   
 395  #========================================================== 
 396  # convenience functions 
 397  #---------------------------------------------------------- 
 411   
 412  #========================================================== 
 413  # startup helpers 
 414  #---------------------------------------------------------- 
415 -def setup_fault_handler(target=None):
416 if target is None: 417 faulthandler.enable() 418 _pre_log_buffer.append('<faulthandler> enabled, target = [console]: %s' % faulthandler) 419 return 420 _pre_log_buffer.append('<faulthandler> enabled, target = [%s]: %s' % (target, faulthandler)) 421 faulthandler.enable(file = target)
422 423 #==========================================================
424 -def setup_console_encoding():
425 print_lines = [] 426 try: 427 sys.stdout.reconfigure(errors = 'surrogateescape') 428 sys.stderr.reconfigure(errors = 'surrogateescape') 429 _pre_log_buffer.append('stdout/stderr reconfigured to use <surrogateescape> for encoding errors') 430 return 431 except AttributeError: 432 line = 'cannot reconfigure sys.stdout/stderr to use <errors="surrogateescape"> (needs Python 3.7+)' 433 _pre_log_buffer.append(line) 434 print_lines.append(line) 435 try: 436 _pre_log_buffer.append('sys.stdout/stderr default to "${PYTHONIOENCODING}=%s"' % os.environ['PYTHONIOENCODING']) 437 return 438 except KeyError: 439 lines = [ 440 '${PYTHONIOENCODING} is not set up, use <PYTHONIOENCODING=utf-8:surrogateescape> in the shell (for Python < 3.7)', 441 'console encoding errors may occur' 442 ] 443 for line in lines: 444 print_lines.append(line) 445 _pre_log_buffer.append(line) 446 for line in print_lines: 447 print('GNUmed startup:', line)
448 449 #==========================================================
450 -def setup_python_path():
451 452 if not '--local-import' in sys.argv: 453 _pre_log_buffer.append('running against systemwide install') 454 return 455 456 local_python_import_dir = os.path.dirname ( 457 os.path.abspath(os.path.join(sys.argv[0], '..')) 458 ) 459 print("Running from local source tree (%s) ..." % local_python_import_dir) 460 _pre_log_buffer.append("running from local source tree: %s" % local_python_import_dir) 461 462 # does the path exist at all, physically ? 463 # (*broken* links are reported as False) 464 link_name = os.path.join(local_python_import_dir, 'Gnumed') 465 if os.path.exists(link_name): 466 _pre_log_buffer.append('local module import dir symlink exists: %s' % link_name) 467 else: 468 real_dir = os.path.join(local_python_import_dir, 'client') 469 print('Creating local module import symlink ...') 470 print(' real dir:', real_dir) 471 print(' link:', link_name) 472 try: 473 os.symlink(real_dir, link_name) 474 except AttributeError: 475 _pre_log_buffer.append('Windows does not have os.symlink(), resorting to ctypes') 476 result = _symlink_windows(real_dir, link_name) 477 _pre_log_buffer.append('ctypes.windll.kernel32.CreateSymbolicLinkW() exit code: %s', result) 478 _pre_log_buffer.append('created local module import dir symlink: link [%s] => dir [%s]' % (link_name, real_dir)) 479 480 sys.path.insert(0, local_python_import_dir) 481 _pre_log_buffer.append('sys.path with local module import base dir prepended: %s' % sys.path)
482 483 #==========================================================
484 -def setup_local_repo_path():
485 486 local_repo_path = os.path.expanduser(os.path.join ( 487 '~', 488 '.gnumed', 489 'local_code', 490 str(current_client_branch) 491 )) 492 local_wxGladeWidgets_path = os.path.join(local_repo_path, 'Gnumed', 'wxGladeWidgets') 493 494 if not os.path.exists(local_wxGladeWidgets_path): 495 _log.debug('[%s] not found', local_wxGladeWidgets_path) 496 _log.info('local wxGlade widgets repository not available') 497 return 498 499 _log.info('local wxGlade widgets repository found:') 500 _log.info(local_wxGladeWidgets_path) 501 502 if not os.access(local_wxGladeWidgets_path, os.R_OK): 503 _log.error('invalid repo: no read access') 504 return 505 506 all_entries = os.listdir(os.path.join(local_repo_path, 'Gnumed')) 507 _log.debug('repo base contains: %s', all_entries) 508 all_entries.remove('wxGladeWidgets') 509 try: 510 all_entries.remove('__init__.py') 511 except ValueError: 512 _log.error('invalid repo: lacking __init__.py') 513 return 514 try: 515 all_entries.remove('__init__.pyc') 516 except ValueError: 517 pass 518 519 if len(all_entries) > 0: 520 _log.error('insecure repo: additional files or directories found') 521 return 522 523 # repo must be 0700 (rwx------) 524 stat_val = os.stat(local_wxGladeWidgets_path) 525 _log.debug('repo stat(): %s', stat_val) 526 perms = stat.S_IMODE(stat_val.st_mode) 527 _log.debug('repo permissions: %s (octal: %s)', perms, oct(perms)) 528 if perms != 448: # octal 0700 529 if os.name in ['nt']: 530 _log.warning('this platform does not support os.stat() permission checking') 531 else: 532 _log.error('insecure repo: permissions not 0600') 533 return 534 535 print("Activating local wxGlade widgets repository (%s) ..." % local_wxGladeWidgets_path) 536 sys.path.insert(0, local_repo_path) 537 _log.debug('sys.path with repo:') 538 _log.debug(sys.path)
539 540 #==========================================================
541 -def setup_logging():
542 try: 543 from Gnumed.pycommon import gmLog2 as _gmLog2 544 except ImportError: 545 print(import_error_sermon % '\n '.join(sys.path)) 546 sys.exit(1) 547 548 print("Log file:", _gmLog2._logfile.name) 549 setup_fault_handler(target = _gmLog2._logfile) 550 551 global gmLog2 552 gmLog2 = _gmLog2 553 554 global _log 555 _log = logging.getLogger('gm.launcher')
556 557 #==========================================================
558 -def log_startup_info():
559 global _pre_log_buffer 560 if len(_pre_log_buffer) > 0: 561 _log.info('early startup log buffer:') 562 for line in _pre_log_buffer: 563 _log.info(' ' + line) 564 del _pre_log_buffer 565 _log.info('GNUmed client version [%s] on branch [%s]', current_client_version, current_client_branch) 566 _log.info('Platform: %s', platform.uname()) 567 _log.info(('Python %s on %s (%s)' % (sys.version, sys.platform, os.name)).replace('\n', '<\\n>')) 568 try: 569 import lsb_release 570 _log.info('lsb_release: %s', lsb_release.get_distro_information()) 571 except ImportError: 572 pass 573 _log.info('module <sys> info:') 574 attrs2skip = ['__doc__', 'copyright', '__name__', '__spec__'] 575 for attr_name in dir(sys): 576 if attr_name in attrs2skip: 577 continue 578 if attr_name.startswith('set'): 579 continue 580 attr = getattr(sys, attr_name) 581 if not attr_name.startswith('get'): 582 _log.info('%s: %s', attr_name.rjust(30), attr) 583 continue 584 if callable(attr): 585 try: 586 _log.info('%s: %s', attr_name.rjust(30), attr()) 587 except Exception: 588 _log.exception('%s: <cannot log>', attr_name.rjust(30)) 589 continue 590 _log.info('module <platform> info:') 591 attrs2skip = ['__doc__', '__copyright__', '__name__', '__spec__', '__cached__', '__builtins__'] 592 for attr_name in dir(platform): 593 if attr_name in attrs2skip: 594 continue 595 if attr_name.startswith('set'): 596 continue 597 attr = getattr(platform, attr_name) 598 if callable(attr): 599 if attr_name.startswith('_'): 600 _log.info('%s: %s', attr_name.rjust(30), attr) 601 continue 602 try: 603 _log.info('%s: %s', attr_name.rjust(30), attr()) 604 except Exception: 605 _log.exception('%s: <cannot log>', attr_name.rjust(30)) 606 continue 607 _log.info('%s: %s', attr_name.rjust(30), attr) 608 continue 609 _log.info('module <os> info:') 610 for n in os.confstr_names: 611 _log.info('%s: %s', ('confstr[%s]' % n).rjust(40), os.confstr(n)) 612 for n in os.sysconf_names: 613 try: 614 _log.info('%s: %s', ('sysconf[%s]' % n).rjust(40), os.sysconf(n)) 615 except Exception: 616 _log.exception('%s: <invalid> ??', ('sysconf[%s]' % n).rjust(30)) 617 os_attrs = ['name', 'ctermid', 'getcwd', 'get_exec_path', 'getegid', 'geteuid', 'getgid', 'getgroups', 'getlogin', 'getpgrp', 'getpid', 'getppid', 'getresuid', 'getresgid', 'getuid', 'supports_bytes_environ', 'uname', 'get_terminal_size', 'pathconf_names', 'times', 'cpu_count', 'curdir', 'pardir', 'sep', 'altsep', 'extsep', 'pathsep', 'defpath', 'linesep', 'devnull'] 618 for attr_name in os_attrs: 619 attr = getattr(os, attr_name) 620 if callable(attr): 621 _log.info('%s: %s', attr_name.rjust(40), attr()) 622 continue 623 _log.info('%s: %s', attr_name.rjust(40), attr) 624 _log.info('process environment:') 625 for key, val in os.environ.items(): 626 _log.info(' %s: %s' % (('${%s}' % key).rjust(40), val)) 627 import sysconfig 628 _log.info('module <sysconfig> info:') 629 _log.info(' platform [%s] -- python version [%s]', sysconfig.get_platform(), sysconfig.get_python_version()) 630 _log.info(' sysconfig.get_paths():') 631 paths = sysconfig.get_paths() 632 for path in paths: 633 _log.info('%s: %s', path.rjust(40), paths[path]) 634 _log.info(' sysconfig.get_config_vars():') 635 conf_vars = sysconfig.get_config_vars() 636 for var in conf_vars: 637 _log.info('%s: %s', var.rjust(45), conf_vars[var])
638 639 #==========================================================
640 -def setup_console_exception_handler():
641 from Gnumed.pycommon.gmTools import handle_uncaught_exception_console 642 643 sys.excepthook = handle_uncaught_exception_console
644 645 #==========================================================
646 -def setup_cli():
647 from Gnumed.pycommon import gmCfg2 648 649 global _cfg 650 _cfg = gmCfg2.gmCfgData() 651 _cfg.add_cli ( 652 short_options = _known_short_options, 653 long_options = _known_long_options 654 ) 655 656 val = _cfg.get(option = '--debug', source_order = [('cli', 'return')]) 657 if val is None: 658 val = False 659 _cfg.set_option ( 660 option = 'debug', 661 value = val 662 ) 663 664 val = _cfg.get(option = '--slave', source_order = [('cli', 'return')]) 665 if val is None: 666 val = False 667 _cfg.set_option ( 668 option = 'slave', 669 value = val 670 ) 671 672 val = _cfg.get(option = '--skip-update-check', source_order = [('cli', 'return')]) 673 if val is None: 674 val = False 675 _cfg.set_option ( 676 option = 'skip-update-check', 677 value = val 678 ) 679 680 val = _cfg.get(option = '--hipaa', source_order = [('cli', 'return')]) 681 if val is None: 682 val = False 683 _cfg.set_option ( 684 option = 'hipaa', 685 value = val 686 ) 687 688 val = _cfg.get(option = '--local-import', source_order = [('cli', 'return')]) 689 if val is None: 690 val = False 691 _cfg.set_option ( 692 option = 'local-import', 693 value = val 694 ) 695 696 _cfg.set_option ( 697 option = 'client_version', 698 value = current_client_version 699 ) 700 701 _cfg.set_option ( 702 option = 'client_branch', 703 value = current_client_branch 704 )
705 706 #==========================================================
707 -def handle_sig_term(signum, frame):
708 _log.critical('SIGTERM (SIG%s) received, shutting down ...' % signum) 709 gmLog2.flush() 710 print('GNUmed: SIGTERM (SIG%s) received, shutting down ...' % signum) 711 if frame is not None: 712 print('%s::%s@%s' % (frame.f_code.co_filename, frame.f_code.co_name, frame.f_lineno)) 713 714 # FIXME: need to do something useful here 715 716 if _old_sig_term in [None, signal.SIG_IGN]: 717 sys.exit(1) 718 else: 719 _old_sig_term(signum, frame)
720 721 #----------------------------------------------------------
722 -def setup_signal_handlers():
723 global _old_sig_term 724 old_sig_term = signal.signal(signal.SIGTERM, handle_sig_term)
725 726 #==========================================================
727 -def setup_locale():
728 gmI18N.activate_locale() 729 730 td = _cfg.get(option = '--text-domain', source_order = [('cli', 'return')]) 731 l = _cfg.get(option = '--lang-gettext', source_order = [('cli', 'return')]) 732 gmI18N.install_domain(domain = td, language = l, prefer_local_catalog = _cfg.get(option = 'local-import'))
733 734 # # make sure we re-get the default encoding 735 # # in case it changed 736 # gmLog2.set_string_encoding() 737 738 #==========================================================
739 -def handle_help_request():
740 src = [('cli', 'return')] 741 742 help_requested = ( 743 _cfg.get(option = '--help', source_order = src) or 744 _cfg.get(option = '-h', source_order = src) or 745 _cfg.get(option = '-?', source_order = src) 746 ) 747 748 if help_requested: 749 input('\nHit <ENTER> to display commandline help\n') 750 if platform.system() == 'Windows': 751 for line in MANPAGE.split('\n'): 752 print(regex.sub('^\.\w+\s*', '', line, count = 1)) 753 sys.exit(0) 754 755 handle, man_page_fname = tempfile.mkstemp(text = True, suffix = '.1') 756 man_page_file = open(man_page_fname, mode = 'wt', encoding = 'utf8') 757 man_page_file.write(MANPAGE % datetime.date.today().strftime('%x')) 758 man_page_file.close() 759 os.system('man %s' % man_page_fname) 760 sys.exit(0)
761 762 #==========================================================
763 -def handle_version_request():
764 gmTools._client_version = current_client_version 765 766 src = [('cli', 'return')] 767 768 version_requested = ( 769 _cfg.get(option = '--version', source_order = src) or 770 _cfg.get(option = '-V', source_order = src) 771 ) 772 773 if version_requested: 774 775 from Gnumed.pycommon.gmPG2 import map_client_branch2required_db_version, known_schema_hashes 776 777 print('GNUmed version information') 778 print('--------------------------') 779 print('client : %s on branch [%s]' % (current_client_version, current_client_branch)) 780 print('database : %s' % map_client_branch2required_db_version[current_client_branch]) 781 print('schema hash: %s' % known_schema_hashes[map_client_branch2required_db_version[current_client_branch]]) 782 sys.exit(0)
783 784 #==========================================================
785 -def setup_paths_and_files():
786 """Create needed paths in user home directory.""" 787 788 gmd_dir = os.path.expanduser(os.path.join('~', 'gnumed')) 789 dot_gmd_dir = os.path.expanduser(os.path.join('~', '.gnumed')) 790 readme = """GNUmed Electronic Medical Record 791 792 %s/ 793 794 This directory should only ever contain files which the 795 user will come into direct contact with while using the 796 application (say, by selecting a file from the file system, 797 as when selecting document parts from files). You can create 798 subdirectories here as you see fit for the purpose. 799 800 This directory will also serve as the default directory when 801 GNUmed asks the user to select a directory for storing a 802 file. 803 804 Any files which are NOT intended for direct user interaction 805 but must be configured to live at a known location (say, 806 inter-application data exchange files) should be put under 807 the hidden directory: 808 "%s/" 809 """ % (gmd_dir, dot_gmd_dir) 810 gmTools.mkdir(gmd_dir) 811 gmTools.create_directory_description_file(directory = gmd_dir, readme = readme) 812 # remove old README 813 gmTools.remove_file(os.path.join(gmd_dir, '00_README')) 814 815 gmTools.mkdir(os.path.expanduser(os.path.join(dot_gmd_dir, 'spellcheck'))) 816 err_dir = os.path.expanduser(os.path.join(dot_gmd_dir, 'error_logs')) 817 gmTools.mkdir(err_dir) 818 readme = """This directory should be used for files not intended for user 819 interaction at the file system level (file selection dialogs, 820 file browsers) such as inter-application data exchange files 821 which need to live at a known location.""" 822 gmTools.create_directory_description_file(directory = dot_gmd_dir, readme = readme) 823 gmTools.create_directory_description_file ( 824 directory = err_dir, 825 readme = 'Whenever an unhandled exception is detected a copy of the log file is placed here.\n\nThis directory is subject to systemd-tmpfiles cleaning.' 826 ) 827 828 # wxPython not available yet 829 paths = gmTools.gmPaths(app_name = 'gnumed') 830 print("Temp dir:", paths.tmp_dir) 831 # ensure there's a user-level config file 832 open(os.path.expanduser(os.path.join('~', '.gnumed', 'gnumed.conf')), mode = 'a+t').close() 833 # symlink log file into temporary directory for easier debugging (everything in one place) 834 logfile_link = os.path.join(paths.tmp_dir, 'zzz-gnumed.log') 835 gmTools.mklink (gmLog2._logfile.name, logfile_link, overwrite = False)
836 837 #==========================================================
838 -def setup_date_time():
839 gmDateTime.init()
840 841 #==========================================================
842 -def setup_cfg():
843 """Detect and setup access to GNUmed config file. 844 845 Parts of this will have limited value due to 846 wxPython not yet being available. 847 """ 848 849 enc = gmI18N.get_encoding() 850 paths = gmTools.gmPaths(app_name = 'gnumed') 851 852 candidates = [ 853 # the current working dir 854 ['workbase', os.path.join(paths.working_dir, 'gnumed.conf')], 855 # /etc/gnumed/ 856 ['system', os.path.join(paths.system_config_dir, 'gnumed-client.conf')], 857 # ~/.gnumed/ 858 ['user', os.path.join(paths.user_config_dir, 'gnumed.conf')], 859 # CVS/tgz tree .../gnumed/client/ (IOW a local installation) 860 ['local', os.path.join(paths.local_base_dir, 'gnumed.conf')] 861 ] 862 # --conf-file= 863 explicit_fname = _cfg.get(option = '--conf-file', source_order = [('cli', 'return')]) 864 if explicit_fname is None: 865 candidates.append(['explicit', None]) 866 else: 867 candidates.append(['explicit', explicit_fname]) 868 869 for candidate in candidates: 870 _cfg.add_file_source ( 871 source = candidate[0], 872 file = candidate[1], 873 encoding = enc 874 ) 875 876 # --conf-file given but does not actually exist ? 877 if explicit_fname is not None: 878 if _cfg.source_files['explicit'] is None: 879 _log.error('--conf-file argument does not exist') 880 print(missing_cli_config_file % explicit_fname) 881 sys.exit(1) 882 883 # any config file found at all ? 884 found_any_file = False 885 for f in _cfg.source_files.values(): 886 if f is not None: 887 found_any_file = True 888 break 889 if not found_any_file: 890 _log.error('no config file found at all') 891 print(no_config_files % '\n '.join(candidates)) 892 sys.exit(1) 893 894 # mime type handling sources 895 fname = 'mime_type2file_extension.conf' 896 _cfg.add_file_source ( 897 source = 'user-mime', 898 file = os.path.join(paths.user_config_dir, fname), 899 encoding = enc 900 ) 901 _cfg.add_file_source ( 902 source = 'system-mime', 903 file = os.path.join(paths.system_config_dir, fname), 904 encoding = enc 905 )
906 907 #==========================================================
908 -def setup_backend_environment():
909 910 db_version = gmPG2.map_client_branch2required_db_version[current_client_branch] 911 _log.info('client expects database version [%s]', db_version) 912 _cfg.set_option ( 913 option = 'database_version', 914 value = db_version 915 ) 916 917 # set up database connection timezone 918 timezone = _cfg.get ( 919 group = 'backend', 920 option = 'client timezone', 921 source_order = [ 922 ('explicit', 'return'), 923 ('workbase', 'return'), 924 ('local', 'return'), 925 ('user', 'return'), 926 ('system', 'return') 927 ] 928 )
929 # if timezone is not None: 930 # gmPG2.set_default_client_timezone(timezone) 931 932 #==========================================================
933 -def run_ui():
934 gmHooks.run_hook_script(hook = 'startup-before-GUI') 935 _use_tui = _cfg.get(option = '--tui', source_order = [('cli', 'return')]) 936 if _use_tui: 937 return run_tui() 938 939 run_gui() 940 gmHooks.run_hook_script(hook = 'shutdown-post-GUI') 941 return 0
942 943 #==========================================================
944 -def run_tui():
945 try: 946 import urwid 947 except ModuleNotFoundError: 948 _log.exception('cannot import <urwid>') 949 return 1 950 951 from Gnumed.urwid import gmTuiMain 952 gmTuiMain.main() 953 return 0
954 955 #==========================================================
956 -def run_gui():
957 958 from Gnumed.wxpython import gmGuiMain 959 profile_file = _cfg.get(option = '--profile', source_order = [('cli', 'return')]) 960 if profile_file is None: 961 gmGuiMain.main() 962 else: 963 _log.info('writing profiling data into %s', profile_file) 964 import profile 965 profile.run('gmGuiMain.main()', profile_file) 966 967 return 0
968 969 #==========================================================
970 -def run_tool():
971 """Run a console tool. 972 973 Exit codes as per man page: 974 0: normal termination of the client 975 < 0: some error occurred while trying to run a console tool 976 -1: an unknown console tool was requested 977 < -1: an error occurred while a console tool was run 978 -999: hard abort of the client 979 980 One of these needs to be returned from this function (and, 981 by extension from the tool having been run, if any). 982 """ 983 tool = _cfg.get(option = '--tool', source_order = [('cli', 'return')]) 984 if tool is None: 985 # not running a tool 986 return None 987 988 if tool not in _known_tools: 989 _log.error('unknown tool requested: %s', tool) 990 print('GNUmed startup: Unknown tool [%s] requested.' % tool) 991 print('GNUmed startup: Known tools: %s' % _known_tools) 992 return -1 993 994 print('') 995 print('==============================================') 996 print('Running tool: %s' % tool) 997 print('----------------------------------------------') 998 print('') 999 1000 if tool == 'generate_man_page': 1001 man_page_fname = os.path.abspath(os.path.join('.', 'gnumed.1')) 1002 man_page_file = open(man_page_fname, mode = 'wt', encoding = 'utf8') 1003 man_page_file.write(MANPAGE % datetime.date.today().strftime('%x')) 1004 man_page_file.close() 1005 print('MAN page saved as:', man_page_fname) 1006 return 0 1007 1008 login, creds = gmPG2.request_login_params() 1009 pool = gmConnectionPool.gmConnectionPool() 1010 pool.credentials = creds 1011 print('') 1012 1013 if tool == 'read_all_rows_of_table': 1014 result = gmPG2.read_all_rows_of_table() 1015 if result in [None, True]: 1016 print('Success.') 1017 return 0 1018 print('Failed. Check the log for details.') 1019 return -2 1020 1021 if tool == 'check_mimetypes_in_archive': 1022 from Gnumed.business import gmDocuments 1023 return gmDocuments.check_mimetypes_in_archive() 1024 1025 if tool == 'check_enc_epi_xref': 1026 from Gnumed.business import gmEMRStructItems 1027 return gmEMRStructItems.check_fk_encounter_fk_episode_x_ref() 1028 1029 if tool == 'export_pat_emr_structure': 1030 # setup praxis 1031 from Gnumed.business import gmPraxis 1032 praxis = gmPraxis.gmCurrentPraxisBranch(branch = gmPraxis.get_praxis_branches()[0]) 1033 # get patient 1034 from Gnumed.business import gmPersonSearch 1035 pat = gmPersonSearch.ask_for_patient() 1036 # setup exporters 1037 from Gnumed.business import gmEMRStructItems 1038 from Gnumed.exporters import gmTimelineExporter 1039 from Gnumed.exporters import gmPatientExporter 1040 while pat is not None: 1041 print('patient:', pat['description_gender']) 1042 # as EMR structure 1043 fname = os.path.expanduser('~/gnumed/gm-emr_structure-%s.txt' % pat.subdir_name) 1044 print('EMR structure:', gmEMRStructItems.export_emr_structure(patient = pat, filename = fname)) 1045 # as timeline 1046 fname = os.path.expanduser('~/gnumed/gm-emr-%s.timeline' % pat.subdir_name) 1047 try: 1048 print('EMR timeline:', gmTimelineExporter.create_timeline_file ( 1049 patient = pat, 1050 filename = fname, 1051 include_documents = True, 1052 include_vaccinations = True, 1053 include_encounters = True 1054 )) 1055 finally: 1056 pass 1057 # as journal by encounter 1058 exporter = gmPatientExporter.cEMRJournalExporter() 1059 fname = os.path.expanduser('~/gnumed/gm-emr-journal_by_encounter-%s.txt' % pat.subdir_name) 1060 print('EMR journal (by encounter):', exporter.save_to_file_by_encounter(patient = pat, filename = fname)) 1061 # as journal by mod time 1062 fname = os.path.expanduser('~/gnumed/gm-emr-journal_by_mod_time-%s.txt' % pat.subdir_name) 1063 print('EMR journal (by mod time):', exporter.save_to_file_by_mod_time(patient = pat, filename = fname)) 1064 # as statistical summary 1065 fname = os.path.expanduser('~/gnumed/gm-emr-statistics-%s.txt' % pat.subdir_name) 1066 output_file = open(fname, mode = 'wt', encoding = 'utf8', errors = 'replace') 1067 emr = pat.emr 1068 output_file.write(emr.format_statistics()) 1069 output_file.close() 1070 print('EMR statistics:', fname) 1071 # as text file 1072 exporter = gmPatientExporter.cEmrExport(patient = pat) 1073 fname = os.path.expanduser('~/gnumed/gm-emr-text_export-%s.txt' % pat.subdir_name) 1074 output_file = open(fname, mode = 'wt', encoding = 'utf8', errors = 'replace') 1075 exporter.set_output_file(output_file) 1076 exporter.dump_constraints() 1077 exporter.dump_demographic_record(True) 1078 exporter.dump_clinical_record() 1079 exporter.dump_med_docs() 1080 output_file.close() 1081 print('EMR text file:', fname) 1082 # another patient ? 1083 pat = gmPersonSearch.ask_for_patient() 1084 1085 return 0 1086 1087 # tool export_patient_as (vcf, gdt, ...) 1088 #if tool == 'export_pat_demographics': 1089 1090 # should not happen (because checked against _known_tools) 1091 return -1
1092 1093 #========================================================== 1094 # shutdown helpers 1095 #----------------------------------------------------------
1096 -def shutdown_backend():
1097 gmPG2.shutdown()
1098 1099 #==========================================================
1100 -def shutdown_logging():
1101 1102 # if _cfg.get(option = u'debug'): 1103 # import types 1104 1105 # def get_refcounts(): 1106 # refcount = {} 1107 # # collect all classes 1108 # for module in sys.modules.values(): 1109 # for sym in dir(module): 1110 # obj = getattr(module, sym) 1111 # if type(obj) is types.ClassType: 1112 # refcount[obj] = sys.getrefcount(obj) 1113 # # sort by refcount 1114 # pairs = map(lambda x: (x[1],x[0]), refcount.items()) 1115 # pairs.sort() 1116 # pairs.reverse() 1117 # return pairs 1118 1119 # rcfile = open('./gm-refcount.lst', 'wt', encoding = 'utf8') 1120 # for refcount, class_ in get_refcounts(): 1121 # if not class_.__name__.startswith('wx'): 1122 # rcfile.write(u'%10d %s\n' % (refcount, class_.__name__)) 1123 # rcfile.close() 1124 1125 # do not choke on Windows 1126 logging.raiseExceptions = False
1127 1128 #==========================================================
1129 -def shutdown_tmp_dir():
1130 1131 tmp_dir = gmTools.gmPaths().tmp_dir 1132 1133 if _cfg.get(option = 'debug'): 1134 _log.debug('not removing tmp dir (--debug mode): %s', tmp_dir) 1135 return 1136 1137 _log.warning('removing tmp dir: %s', tmp_dir) 1138 shutil.rmtree(tmp_dir, True)
1139 1140 #========================================================== 1141 # main - launch the GNUmed wxPython GUI client 1142 #---------------------------------------------------------- 1143 1144 random.seed() 1145 1146 # setup 1147 setup_fault_handler(target = None) 1148 setup_console_encoding() 1149 setup_python_path() 1150 setup_logging() 1151 log_startup_info() 1152 setup_console_exception_handler() 1153 setup_cli() 1154 setup_signal_handlers() 1155 setup_local_repo_path() 1156 1157 from Gnumed.pycommon import gmI18N 1158 from Gnumed.pycommon import gmTools 1159 from Gnumed.pycommon import gmDateTime 1160 1161 setup_locale() 1162 handle_help_request() 1163 handle_version_request() 1164 setup_paths_and_files() 1165 setup_date_time() 1166 setup_cfg() 1167 1168 from Gnumed.pycommon import gmPG2 1169 from Gnumed.pycommon import gmConnectionPool 1170 setup_backend_environment() 1171 1172 # main 1173 exit_code = run_tool() 1174 if exit_code is None: 1175 from Gnumed.pycommon import gmHooks 1176 exit_code = run_ui() 1177 1178 # shutdown 1179 shutdown_backend() 1180 shutdown_tmp_dir() 1181 _log.info('Normally shutting down as main module.') 1182 shutdown_logging() 1183 1184 sys.exit(exit_code) 1185 1186 #========================================================== 1187