gnuhealth-control 5.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,76 @@
1
+ Metadata-Version: 2.3
2
+ Name: gnuhealth-control
3
+ Version: 5.0.0
4
+ Summary: GNU Health HIS installer and instance administration tool
5
+ License: GPL-3.0-or-later
6
+ Keywords: health,hospital,HIS,eHealth,gnuhealth
7
+ Author: GNU Solidario
8
+ Author-email: health@gnusolidario.org
9
+ Maintainer: Luis Falcon
10
+ Maintainer-email: falcon@gnuhealth.org
11
+ Requires-Python: >=3.10,<3.14
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Environment :: Console :: Curses
14
+ Classifier: Intended Audience :: Healthcare Industry
15
+ Classifier: Intended Audience :: System Administrators
16
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
17
+ Classifier: Natural Language :: English
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
25
+ Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
26
+ Requires-Dist: proteus (>=7.0.0,<7.1)
27
+ Requires-Dist: psutil (>=7.0.0,<8.0.0)
28
+ Project-URL: Bug Tracker, https://codeberg.org/gnuhealth/his-utils/issues
29
+ Project-URL: Documentation, https://docs.gnuhealth.org
30
+ Project-URL: Homepage, https://www.gnuhealth.org
31
+ Project-URL: Repository, https://codeberg.org/gnuhealth/his-utils
32
+ Description-Content-Type: text/x-rst
33
+
34
+ .. SPDX-FileCopyrightText: 2008-2025 Luis Falcón <falcon@gnuhealth.org>
35
+ .. SPDX-FileCopyrightText: 2011-2025 GNU Solidario <health@gnusolidario.org>
36
+ ..
37
+ .. SPDX-License-Identifier: CC-BY-SA-4.0
38
+
39
+ The GNU Health HIS Installer and Manager
40
+ ========================================
41
+
42
+ The gnuhealth-control is a single-entry point to install, setup and manage
43
+ the GNU Health Hospital Management System
44
+
45
+ Installation
46
+ ------------
47
+ GNU Health Control application can be installed with pip::
48
+
49
+ $ pip install --upgrade gnuhealth-control
50
+
51
+
52
+ Technology
53
+ ----------
54
+ The GNU Health Control is a self-contained curses Python application.
55
+
56
+
57
+ Development
58
+ -----------
59
+ The development of gnuhealth-control will be done at Codeberg.
60
+
61
+ The development mailing list is at health-dev@gnu.org
62
+ General questions can be done on health@gnu.org mailing list.
63
+
64
+ Note: You need to subscribe before posting or the messages will be automatically
65
+ discarded. More at: https://docs.gnuhealth.org/his/support.html
66
+
67
+ Homepage
68
+ --------
69
+ https://www.gnuhealth.org
70
+
71
+
72
+ Documentation
73
+ -------------
74
+
75
+ https://docs.gnuhealth.org
76
+
@@ -0,0 +1,42 @@
1
+ .. SPDX-FileCopyrightText: 2008-2025 Luis Falcón <falcon@gnuhealth.org>
2
+ .. SPDX-FileCopyrightText: 2011-2025 GNU Solidario <health@gnusolidario.org>
3
+ ..
4
+ .. SPDX-License-Identifier: CC-BY-SA-4.0
5
+
6
+ The GNU Health HIS Installer and Manager
7
+ ========================================
8
+
9
+ The gnuhealth-control is a single-entry point to install, setup and manage
10
+ the GNU Health Hospital Management System
11
+
12
+ Installation
13
+ ------------
14
+ GNU Health Control application can be installed with pip::
15
+
16
+ $ pip install --upgrade gnuhealth-control
17
+
18
+
19
+ Technology
20
+ ----------
21
+ The GNU Health Control is a self-contained curses Python application.
22
+
23
+
24
+ Development
25
+ -----------
26
+ The development of gnuhealth-control will be done at Codeberg.
27
+
28
+ The development mailing list is at health-dev@gnu.org
29
+ General questions can be done on health@gnu.org mailing list.
30
+
31
+ Note: You need to subscribe before posting or the messages will be automatically
32
+ discarded. More at: https://docs.gnuhealth.org/his/support.html
33
+
34
+ Homepage
35
+ --------
36
+ https://www.gnuhealth.org
37
+
38
+
39
+ Documentation
40
+ -------------
41
+
42
+ https://docs.gnuhealth.org
@@ -0,0 +1,53 @@
1
+ # SPDX-FileCopyrightText: 2025 GNU Solidario <health@gnusolidario.org>
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ [build-system]
6
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
7
+ build-backend = "poetry.core.masonry.api"
8
+
9
+ [project]
10
+ name = "gnuhealth-control"
11
+ version = "5.0.0"
12
+ description = "GNU Health HIS installer and instance administration tool"
13
+ license = { text = "GPL-3.0-or-later" }
14
+ readme = "README.rst"
15
+ keywords = ["health", "hospital", "HIS", "eHealth", "gnuhealth"]
16
+ requires-python = ">=3.10,<3.14"
17
+ authors = [
18
+ {name = "GNU Solidario", email = "health@gnusolidario.org"},
19
+ ]
20
+
21
+ dynamic = [ "classifiers" ]
22
+
23
+ maintainers = [
24
+ { name = "Luis Falcon", email = "falcon@gnuhealth.org" },
25
+ ]
26
+
27
+ dependencies = [
28
+ "psutil (>=7.0.0,<8.0.0)",
29
+ "proteus (>=7.0.0,<7.1)",
30
+ ]
31
+
32
+
33
+ [project.urls]
34
+ homepage = "https://www.gnuhealth.org"
35
+ repository = "https://codeberg.org/gnuhealth/his-utils"
36
+ documentation = "https://docs.gnuhealth.org"
37
+ "Bug Tracker" = "https://codeberg.org/gnuhealth/his-utils/issues"
38
+
39
+ [tool.poetry]
40
+ classifiers=[
41
+ 'Development Status :: 5 - Production/Stable',
42
+ 'Environment :: Console :: Curses',
43
+ 'Intended Audience :: System Administrators',
44
+ 'Intended Audience :: Healthcare Industry',
45
+ 'Natural Language :: English',
46
+ 'Operating System :: OS Independent',
47
+ 'Programming Language :: Python :: 3',
48
+ 'Topic :: Scientific/Engineering :: Bio-Informatics',
49
+ 'Topic :: Scientific/Engineering :: Medical Science Apps.',
50
+ ]
51
+
52
+ [project.scripts]
53
+ ghcontrol = 'gnuhealth_control.ghcontrol:main'
@@ -0,0 +1,1185 @@
1
+ # SPDX-FileCopyrightText: 2008-2025 Luis Falcón <falcon@gnuhealth.org> #
2
+ # SPDX-FileCopyrightText: 2011-2025 GNU Solidario <health@gnusolidario.org> #
3
+ # #
4
+ # SPDX-License-Identifier: GPL-3.0-or-later #
5
+ #############################################################################
6
+ # Hospital Management Information System (HMIS) component of the #
7
+ # GNU Health project #
8
+ # https://www.gnuhealth.org #
9
+ #############################################################################
10
+ # ghcontrol.py #
11
+ #############################################################################
12
+
13
+ from datetime import datetime
14
+ from curses import wrapper, panel
15
+ from venv import EnvBuilder
16
+ from shutil import copy
17
+ from importlib.metadata import distributions
18
+
19
+ import argparse
20
+ import curses
21
+ import os
22
+ import re
23
+ import subprocess
24
+ import psutil
25
+ import string
26
+ import random
27
+
28
+ # ghcontrol version shares release major and minor numbers with GH HIS.
29
+ VERSION = "5.0.0" # gnuheath-control version
30
+ MIN_VER = '>=5.0.0a1,<5.1' # Initial gnuhealth server package version
31
+ TRYTON_PINNING = '>=7.0,<7.1' # Pin the tryton packages to this range
32
+
33
+ """ GNU Health control limits the health packages to the official ones
34
+ for a particular release
35
+ """
36
+
37
+ HEALTH_PACKAGES = [
38
+ "archives", "caldav", "calendar", "contact-tracing", "crypto",
39
+ "crypto-lab", "dentistry", "disability", "ems", "federation", "genetics",
40
+ "genetics-uniprot", "gyneco", "history", "icd10", "icd10pcs", "icd11",
41
+ "icd9procs", "icpm", "icu", "imaging", "imaging-worklist", "inpatient",
42
+ "inpatient-calendar", "insurance", "iss", "lab", "lifestyle", "mdg6",
43
+ "ntd", "ntd-chagas", "ntd-dengue", "nursing", "ophthalmology", "orthanc",
44
+ "pediatrics", "pediatrics-growth-charts", "pediatrics-growth-charts-who",
45
+ "qrcodes", "reporting", "services", "services-imaging", "services-lab",
46
+ "socioeconomics", "stock", "stock-inpatient", "stock-nursing",
47
+ "stock-surgery", "surgery", "surgery-protocols", "webdav3-server",
48
+ "who-essential-medicines"]
49
+
50
+
51
+ def log_window():
52
+ winlen = 45
53
+ x = int(curses.COLS/2 - winlen/2)
54
+ logwin = curses.newwin(10, winlen, 2, x)
55
+ logwin.box()
56
+ curses.echo()
57
+ return logwin
58
+
59
+
60
+ def show_logs(win, row, col, data, attr=None):
61
+ win.addstr(row, col, data, attr)
62
+ win.refresh()
63
+
64
+
65
+ def task_engine(task, window, row):
66
+ task_name = task[0][0]
67
+ args = task[0][1]
68
+ tsk = task[1]
69
+
70
+ task_str = f"Running {task_name} ..."
71
+ show_logs(win=window, row=row, col=1, data=task_str, attr=0)
72
+ window.refresh()
73
+
74
+ rc = tsk(args)
75
+ if rc == 0:
76
+ attr = curses.color_pair(1) | curses.A_BOLD
77
+ result = "[OK]"
78
+ else:
79
+ attr = curses.color_pair(2) | curses.A_BOLD
80
+ result = "[ERROR]"
81
+ col = 35
82
+ show_logs(win=window, row=row, col=col, data=result, attr=attr)
83
+ window.refresh()
84
+ return rc
85
+
86
+
87
+ def get_timestamp():
88
+ return datetime.now().strftime("%Y%m%d%H%M%S")
89
+
90
+
91
+ class Server:
92
+
93
+ arguments = None
94
+
95
+ def __init__(self, stdscr, arguments):
96
+ self.arguments = arguments
97
+ self.stdscr = stdscr
98
+
99
+ def get_base_dir(self):
100
+ basedir = self.arguments.basedir
101
+ return basedir
102
+
103
+ def get_his_dir(self):
104
+ basedir = self.get_base_dir()
105
+ major_minor = re.match(r'(^[0-9]+)\.([0-9]+)\.(.+$)', VERSION)
106
+ rel = f"{major_minor[1]}{major_minor[2]}"
107
+ release = self.arguments.release or rel
108
+ his_dir = f"{basedir}/his-{release}"
109
+ return his_dir
110
+
111
+ def get_pip_cache_dir(self):
112
+ basedir = self.get_base_dir()
113
+ pip_cache_dir = f"{basedir}/cache/pip"
114
+ return pip_cache_dir
115
+
116
+ def setup_dirs(self):
117
+ """ Creates the basic directory structure to hold GH HIS
118
+ components
119
+ """
120
+
121
+ basedir = self.get_base_dir()
122
+ his_dir = self.get_his_dir()
123
+ pip_cache_dir = self.get_pip_cache_dir()
124
+
125
+ try:
126
+ # Create root directory
127
+ os.makedirs(basedir)
128
+
129
+ # Create pip cache directory
130
+ os.makedirs(pip_cache_dir, exist_ok=True)
131
+
132
+ except FileExistsError: # Allow the basedir to exist
133
+ pass
134
+
135
+ try:
136
+ # Create his directory
137
+ os.mkdir(his_dir)
138
+
139
+ except BaseException:
140
+ return -1
141
+
142
+ for subdir in ["etc", "log", "local", "attach", "backup"]:
143
+ try:
144
+ # Create subdirs
145
+ os.mkdir(f"{his_dir}/{subdir}")
146
+ except BaseException:
147
+ return -1
148
+
149
+ return 0
150
+
151
+ def setup_virtualenv(self):
152
+ """ Create the virtual environment
153
+ """
154
+ his_dir = self.get_his_dir()
155
+ venvdir = f"{his_dir}/venv"
156
+ ghvenv = EnvBuilder(system_site_packages=False, with_pip=True)
157
+
158
+ try:
159
+ ghvenv.create(env_dir=venvdir)
160
+ except BaseException:
161
+ return -1
162
+
163
+ return 0
164
+
165
+ def install_core(self):
166
+ """ Install the core package ("gnuhealth")
167
+ in the newly created virtual environment
168
+ """
169
+ his_dir = self.get_his_dir()
170
+ pip_cache_dir = self.get_pip_cache_dir()
171
+ npython = f"{his_dir}/venv/bin/python"
172
+
173
+ """ The following vars will only be used in development
174
+ """
175
+ pre = self.arguments.pre
176
+ test_repo = "https://test.pypi.org/simple/"
177
+ extra_index = "https://pypi.org/simple/"
178
+ deplog = f"{his_dir}/log/install_deps.log"
179
+ with open(deplog, "w") as logfile:
180
+ if (self.arguments.test):
181
+ process = [
182
+ npython, '-m', 'pip', 'install', '-i', test_repo,
183
+ '--extra-index-url', extra_index,
184
+ '--cache-dir', pip_cache_dir,
185
+ '--upgrade', f"gnuhealth{MIN_VER}"]
186
+ else:
187
+ process = [
188
+ npython, '-m', 'pip', 'install', '--cache-dir',
189
+ pip_cache_dir, '--upgrade',
190
+ f"gnuhealth{MIN_VER}"]
191
+
192
+ if pre:
193
+ process.append('--pre') # Append pip pre-release argument
194
+
195
+ try:
196
+ subprocess.run(process, stdout=logfile, stderr=logfile)
197
+
198
+ except BaseException:
199
+ return -1
200
+
201
+ return 0
202
+
203
+ def check_config_dir(self):
204
+ his_dir = self.get_his_dir()
205
+ etc_dir = f"{his_dir}/etc"
206
+
207
+ if os.path.exists(etc_dir) and os.path.isdir(etc_dir):
208
+ return 0
209
+ else:
210
+ return -1
211
+
212
+ def gnuhealthrc(self):
213
+ """ Generate the gnuhealthrc profile.
214
+ We explicitly set the virtual environment (VIRTUAL_ENV)
215
+ and path so no need to source 'activate' script
216
+ Set aliases and editor for interactive sessions
217
+ For non-interactive sessions there are also the following
218
+ scripts:
219
+ * start_gnuhealth
220
+ * editconf
221
+ * ghis_env
222
+ """
223
+ his_dir = self.get_his_dir()
224
+ venvdir = f"{his_dir}/venv"
225
+ trytond_cfg = f"{his_dir}/etc/trytond.conf"
226
+ trytond_log_conf = f"{his_dir}/etc/server_log.conf"
227
+
228
+ ghrc = f'{his_dir}/etc/gnuhealthrc'
229
+ self.create_bash_wrap(
230
+ file_name="etc/gnuhealthrc",
231
+ executable=False,
232
+ need_backup=True,
233
+ file_content=f"""\
234
+ export VIRTUAL_ENV={venvdir}
235
+ export PATH={his_dir}:{venvdir}/bin:$PATH
236
+ export TRYTOND_CONFIG={trytond_cfg}
237
+ export TRYTOND_LOGGING_CONFIG={trytond_log_conf}
238
+ export GNUHEALTH_HIS_BASE={his_dir}
239
+
240
+ alias cdlogs='cd {his_dir}/log'
241
+ alias cdbase='cd {his_dir}'
242
+
243
+ # Avoid accidental execution of rm, mv or cp
244
+ alias | grep rm= &> /dev/null || alias rm='rm -i'
245
+ alias | grep mv= &> /dev/null || alias mv='mv -i'
246
+ alias | grep cp= &> /dev/null || alias cp='cp -i'
247
+ """)
248
+
249
+ ghenv = f'{his_dir}/ghis_env'
250
+ self.create_bash_wrap("ghis_env", f"""\
251
+ #!/usr/bin/env bash
252
+
253
+ [[ -f {venvdir}/bin/activate ]] && source {venvdir}/bin/activate
254
+ [[ -f {ghrc} ]] && source {ghrc}
255
+
256
+ export GNUHEALTH_ENV=1
257
+
258
+ exec \"$@\"
259
+ """)
260
+ self.create_bash_wrap("start_gnuhealth", f"""\
261
+ #!/usr/bin/env bash
262
+
263
+ {ghenv} trytond \"$@\"
264
+ """)
265
+
266
+ self.create_bash_wrap("editconf", f"""\
267
+ #!/usr/bin/env bash
268
+
269
+ if command -v nano &> /dev/null; then
270
+ export EDITOR=nano
271
+ else
272
+ export EDITOR=vi
273
+ fi
274
+
275
+ $EDITOR {trytond_cfg} \"$@\"
276
+ """)
277
+
278
+ self.modify_bashrc(ghrc)
279
+
280
+ return 0
281
+
282
+ def create_bash_wrap(self, file_name, file_content,
283
+ executable=True,
284
+ need_backup=False):
285
+ his_dir = self.get_his_dir()
286
+ file_path = f'{his_dir}/{file_name}'
287
+ timestamp = self.get_timestamp()
288
+
289
+ if need_backup:
290
+ file_backup = f"{file_path}.back-{timestamp}"
291
+ if os.path.isfile(file_path):
292
+ copy(file_path, file_backup)
293
+
294
+ with open(file_path, "w") as f:
295
+ file_content = file_content
296
+ f.write(file_content)
297
+ if executable:
298
+ os.chmod(file_path, 0o755)
299
+
300
+ def get_timestamp(self):
301
+ return datetime.now().strftime("%Y%m%d%H%M%S")
302
+
303
+ def modify_bashrc(self, ghrc):
304
+ home = os.environ['HOME']
305
+ bashrc = f"{home}/.bashrc"
306
+ timestamp = self.get_timestamp()
307
+ bashrc_back = f"{home}/.bashrc.back-{timestamp}"
308
+
309
+ if not self.arguments.ignore_bashrc:
310
+ if os.path.isfile(bashrc):
311
+ copy(bashrc, bashrc_back) # Make a backup of bashrc
312
+ self.modify_bashrc_gnuhealth_section(
313
+ bashrc, f"[[ -f {ghrc} ]] && source {ghrc}")
314
+
315
+ # --update-config will also update $HOME/.bashrc
316
+ if (self.arguments.update_config
317
+ and self.arguments.ignore_bashrc):
318
+ if os.path.isfile(bashrc):
319
+ copy(bashrc, bashrc_back) # Make a backup of bashrc
320
+ self.modify_bashrc_gnuhealth_section(bashrc, "")
321
+
322
+ def modify_bashrc_gnuhealth_section(self, file_path, content):
323
+
324
+ start_marker = "### GNUHEALTH_BEGIN_SETTING ###"
325
+ end_marker = "### GNUHEALTH_END_SETTING ###"
326
+
327
+ start_index = -1
328
+ end_index = -1
329
+
330
+ if os.path.isfile(file_path): # Check for existance of the file
331
+ with open(file_path, 'r') as f:
332
+ lines = f.readlines()
333
+
334
+ for i, line in enumerate(lines):
335
+ if start_marker in line:
336
+ start_index = i
337
+ if end_marker in line:
338
+ end_index = i
339
+
340
+ if start_index == -1 or end_index == -1:
341
+ with open(file_path, "a") as f:
342
+ f.write(f'\n{start_marker}\n{content}\n{end_marker}\n')
343
+
344
+ else:
345
+ new_lines = lines[:start_index + 1] + \
346
+ [content + "\n"] + lines[end_index:]
347
+
348
+ with open(file_path, 'w') as f:
349
+ f.writelines(new_lines)
350
+
351
+ def trytond_cfg(self):
352
+ """ Generate trytond.conf
353
+ and server_log.conf
354
+ """
355
+ his_dir = self.get_his_dir()
356
+ timestamp = self.get_timestamp()
357
+ trytond_cfg = f"{his_dir}/etc/trytond.conf"
358
+ trytond_cfg_back = f"{his_dir}/etc/trytond.conf.back-{timestamp}"
359
+
360
+ trytond_log_conf = f"{his_dir}/etc/server_log.conf"
361
+ trytond_log_conf_back = \
362
+ f"{his_dir}/etc/server_log.conf.back-{timestamp}"
363
+
364
+ if os.path.isfile(trytond_cfg): # If the profile exists, make a backup
365
+ copy(trytond_cfg, trytond_cfg_back)
366
+
367
+ if os.path.isfile(trytond_log_conf): # If it exists, make a backup
368
+ copy(trytond_log_conf, trytond_log_conf_back)
369
+
370
+ with open(trytond_cfg, "w") as trytondconf_file:
371
+ file_content = f"""\
372
+ # Generated by gnuhealth-control
373
+ [database]
374
+ uri = postgresql://localhost:5432
375
+ path = {his_dir}/attach
376
+
377
+ [web]
378
+ # Listen to all network interfaces.
379
+ listen = 0.0.0.0:8000
380
+ """
381
+ trytondconf_file.write(file_content)
382
+
383
+ with open(trytond_log_conf, "w") as logconf_file:
384
+ file_content = f"""\
385
+ [formatters]
386
+ keys=simple
387
+
388
+ [handlers]
389
+ keys=rotate,console
390
+
391
+ [loggers]
392
+ keys=root
393
+
394
+ [formatter_simple]
395
+ format=[%(asctime)s] %(levelname)s:%(name)s:%(message)s
396
+ datefmt=%a %b %d %H:%M:%S %Y
397
+
398
+ [handler_rotate]
399
+ class=handlers.TimedRotatingFileHandler
400
+ args=('{his_dir}/log/his_server.log', 'D', 1, 30)
401
+ formatter=simple
402
+
403
+ [handler_console]
404
+ class=StreamHandler
405
+ formatter=simple
406
+ args=(sys.stdout,)
407
+
408
+ [logger_root]
409
+ level=INFO
410
+ handlers=rotate,console
411
+ """
412
+
413
+ logconf_file.write(file_content)
414
+
415
+ return 0
416
+
417
+ def install_finished(self):
418
+ return 0
419
+
420
+ def do_task(self, task):
421
+ """ Method to execute each of the tasks
422
+ and get the result code
423
+ """
424
+
425
+ tsk = getattr(self, task)
426
+ rc = tsk()
427
+ return (rc)
428
+
429
+ def setup(self):
430
+ """ Installs a GNU Health HIS Server
431
+ """
432
+ if self.arguments.update_config:
433
+ TASKS = ["check_config_dir", "gnuhealthrc",
434
+ "trytond_cfg", "install_finished"]
435
+ else:
436
+ TASKS = ["setup_dirs", "setup_virtualenv", "install_core",
437
+ "gnuhealthrc", "trytond_cfg", "install_finished"]
438
+ logwin = log_window()
439
+ row = 1
440
+
441
+ for task in TASKS:
442
+ if task == 'install_finished':
443
+ task_str = "Installation successful"
444
+ msg = "Please LOGOUT and re-login " \
445
+ "to activate the new environment"
446
+
447
+ attr = curses.color_pair(3) | curses.A_BOLD
448
+ x = int(curses.COLS/2 - len(msg)/2)
449
+ msgwin = curses.newwin(4, len(msg)+1, 13, x)
450
+ msgwin.addstr(2, 1, msg, attr)
451
+ msgwin.refresh()
452
+
453
+ else:
454
+ task_str = f"Running {task} ..."
455
+ show_logs(win=logwin, row=row, col=1, data=task_str, attr=0)
456
+ logwin.refresh()
457
+ rc = self.do_task(task)
458
+ if rc == 0:
459
+ attr = curses.color_pair(1) | curses.A_BOLD
460
+ result = "[OK]"
461
+ else:
462
+ attr = curses.color_pair(2) | curses.A_BOLD
463
+ result = "[ERROR]"
464
+ col = 30
465
+ show_logs(
466
+ win=logwin, row=row, col=col, data=result, attr=attr)
467
+ row = row + 1
468
+ logwin.refresh()
469
+ if rc != 0:
470
+ break
471
+
472
+ def start(self):
473
+ """ Start the GNU Health HIS Tryton server
474
+ """
475
+ nenv = os.environ.copy()
476
+ try:
477
+ subprocess.Popen(
478
+ ['trytond'], env=nenv, stdout=subprocess.DEVNULL,
479
+ stderr=subprocess.STDOUT)
480
+
481
+ except BaseException:
482
+ return -1
483
+
484
+ GHControl.footer_window(self, self.stdscr) # Refresh footer window
485
+ return 0
486
+
487
+ def stop(self):
488
+ """ Stops the GNU Health HIS Tryton server
489
+ """
490
+ for proc in psutil.process_iter():
491
+ # look for combinations that contain both python and trytond in
492
+ # the same command line
493
+ try:
494
+ command_line = proc.cmdline()
495
+ except psutil.ZombieProcess:
496
+ pass
497
+
498
+ if any("python" and "trytond" in args for args in command_line):
499
+ proc.terminate() # Gracefully ask to stop the server
500
+ try:
501
+ proc.wait(timeout=5)
502
+ except BaseException: # If timeout, time to kill...
503
+ proc.kill()
504
+ proc.wait()
505
+
506
+ GHControl.footer_window(self, self.stdscr) # Refresh footer window
507
+ return 0
508
+
509
+ def status(self):
510
+ """ Shows the status of the server
511
+ """
512
+
513
+ for proc in psutil.process_iter():
514
+ # True if process contains python & trytond (
515
+ # (eg, python ./trytond // python trytond ..)
516
+ # We can evaluate other combinations, like when using gunicorn
517
+ try:
518
+ command_line = proc.cmdline()
519
+
520
+ except psutil.ZombieProcess:
521
+ pass
522
+ except psutil.NoSuchProcess:
523
+ pass
524
+
525
+ if any("python" and "trytond" in args for args in command_line):
526
+ return True
527
+ return False
528
+
529
+ def update(self):
530
+ """ Updates to the latest compatible version the existing
531
+ gnuhealth and tryton packages in the virtual environment
532
+ """
533
+ pip_cache_dir = self.get_pip_cache_dir()
534
+ his_dir = self.get_his_dir()
535
+ npython = f"{his_dir}/venv/bin/python"
536
+ updatelog = f"{his_dir}/log/update.log"
537
+ logfile = open(updatelog, "w")
538
+
539
+ win = curses.newwin(10, 50, 3, 30)
540
+ win.box()
541
+ pkg_ok = True
542
+ status = "System Package(s) Update OK"
543
+ attr = curses.color_pair(1) | curses.A_BOLD
544
+
545
+ for package in distributions():
546
+ pkg = package.metadata["Name"]
547
+ win.addstr(1, 2, "Updating....")
548
+ x = 25 - int(len(pkg)/2)
549
+ win.addstr(4, x, f"{pkg}", curses.A_BOLD)
550
+
551
+ win.refresh()
552
+
553
+ if ("tryton" in pkg or "gnuhealth" in pkg):
554
+ if "tryton" in pkg:
555
+ pkg = f"{pkg}{TRYTON_PINNING}"
556
+ else:
557
+ pkg = f"{pkg}{MIN_VER}" # Get GNU Health packages
558
+
559
+ process = [
560
+ npython, '-m', 'pip', 'install', '--cache-dir',
561
+ pip_cache_dir, '--upgrade',
562
+ pkg]
563
+
564
+ try:
565
+ subprocess.check_call(
566
+ process, stdout=logfile, stderr=logfile)
567
+ except BaseException:
568
+ pkg_ok = False
569
+
570
+ win.refresh()
571
+ win.clear()
572
+ win.box()
573
+
574
+ if not pkg_ok:
575
+ attr = curses.color_pair(2) | curses.A_BOLD
576
+ status = "[ERROR] Check logs"
577
+
578
+ x = 25 - int(len(status)/2)
579
+ win.addstr(4, x, status, attr)
580
+ win.refresh()
581
+
582
+ def add_package(self):
583
+ """ Get the package name (without the gnuhealth prefix)
584
+ """
585
+ his_dir = self.get_his_dir()
586
+ pip_cache_dir = self.get_pip_cache_dir()
587
+ npython = f"{his_dir}/venv/bin/python"
588
+ pkglog = f"{his_dir}/log/packages.log"
589
+
590
+ # List the official GNU Health packages
591
+ winwidth = 80
592
+ x = int(curses.COLS/2 - winwidth/2)
593
+ winlist = curses.newwin(12, winwidth, 13, x)
594
+ winlist.box()
595
+ winlist.addstr(1, 2, "Available packages:", curses.color_pair(1))
596
+ row = 3
597
+ col = 2
598
+
599
+ for pkg in HEALTH_PACKAGES:
600
+ if HEALTH_PACKAGES[-1] == pkg:
601
+ pkg = f"{pkg}."
602
+ else:
603
+ pkg = f"{pkg}, "
604
+
605
+ if col + len(pkg) < 80:
606
+ winlist.addstr(row, col, pkg)
607
+ col = col + len(pkg)
608
+ else:
609
+ col = 2
610
+ row = row + 1
611
+ winlist.addstr(row, col, pkg)
612
+ winlist.refresh()
613
+
614
+ win = curses.newwin(10, 50, 3, 30)
615
+ win.box()
616
+ curses.echo()
617
+ curses.curs_set(1)
618
+ win.addstr(2, 1, "Health package:")
619
+ pkg = win.getstr(2, 20).decode()
620
+ win.addstr(4, 1, f"{pkg} selected.")
621
+ win.addstr(6, 1, "Install? (yes/no)")
622
+ confirm = win.getstr(6, 20).decode()
623
+ if (confirm != "yes"):
624
+ return
625
+
626
+ win.clear()
627
+ win.refresh()
628
+ win.box()
629
+
630
+ if (pkg not in HEALTH_PACKAGES and pkg != "all"):
631
+ win.addstr(4, 3, f"ERROR: {pkg} not in list")
632
+ win.refresh()
633
+ return
634
+
635
+ """
636
+ If we answer yes, use pip to install the selected package
637
+ """
638
+
639
+ curses.curs_set(0)
640
+
641
+ logfile = open(pkglog, "w")
642
+
643
+ if pkg == "all":
644
+ packages = HEALTH_PACKAGES
645
+ else:
646
+ packages = [pkg]
647
+
648
+ pkg_ok = True
649
+ status = "Package(s) Installation OK"
650
+ attr = curses.color_pair(1) | curses.A_BOLD
651
+ for pkg in packages:
652
+ pkg_pinning = f"{pkg}{MIN_VER}" # Append version pinning
653
+ win.addstr(1, 2, "Processing....")
654
+ x = 25 - int(len(pkg)/2)
655
+ win.addstr(4, x, f"{pkg}", curses.A_BOLD)
656
+
657
+ win.refresh()
658
+
659
+ process = [
660
+ npython, '-m', 'pip', 'install', '--cache-dir',
661
+ pip_cache_dir, '--upgrade',
662
+ f"gnuhealth-{pkg_pinning}"]
663
+
664
+ try:
665
+ subprocess.check_call(
666
+ process, stdout=logfile, stderr=logfile)
667
+ except BaseException:
668
+ pkg_ok = False
669
+
670
+ win.refresh()
671
+ win.clear()
672
+ win.box()
673
+
674
+ if not pkg_ok:
675
+ attr = curses.color_pair(2) | curses.A_BOLD
676
+ status = "[ERROR] Check logs"
677
+
678
+ x = 25 - int(len(status)/2)
679
+ win.addstr(4, x, status, attr)
680
+ win.refresh()
681
+
682
+
683
+ class Instance:
684
+
685
+ arguments = None
686
+
687
+ def __init__(self, stdscr, arguments):
688
+ self.arguments = arguments
689
+ self.his_base = os.environ.get('GNUHEALTH_HIS_BASE')
690
+
691
+ def show_logs(self, win, row, col, data, attr=None):
692
+ win.addstr(row, col, data, attr)
693
+ win.refresh()
694
+
695
+ def create_db(self, *args):
696
+ """ Create database
697
+ """
698
+ dbname, = args[0]
699
+ dblog = f"{self.his_base}/log/createdb.log"
700
+ with open(dblog, "w") as logfile:
701
+ try:
702
+ subprocess.run(
703
+ ['createdb', dbname],
704
+ stdout=logfile, stderr=logfile)
705
+
706
+ except BaseException:
707
+ return -1
708
+ return 0
709
+
710
+ def create_instance(self, *args):
711
+ """ Create Instance
712
+ """
713
+ dbname = args[0][0]
714
+ email = args[0][1]
715
+ password = args[0][2]
716
+ fname = ''.join(random.choices(string.ascii_lowercase, k=5))
717
+ tryton_pass = f"/tmp/.{fname}"
718
+ with open(tryton_pass, "w") as pwfile:
719
+ try:
720
+ pwfile.write(password)
721
+ except BaseException:
722
+ os.remove(tryton_pass) # Delete temp file
723
+ return -1
724
+
725
+ log = f"{self.his_base}/log/create_instance.log"
726
+ tadmin = f"{self.his_base}/venv/bin/trytond-admin"
727
+
728
+ """ Update the environment variables for this session
729
+ """
730
+ nenv = os.environ.copy()
731
+ nenv['TRYTONPASSFILE'] = tryton_pass
732
+
733
+ with open(log, "w") as logfile:
734
+ try:
735
+ subprocess.check_call(
736
+ [tadmin, '--database', dbname,
737
+ '--email', email, '--all', '-vv'],
738
+ env=nenv, stdout=logfile, stderr=logfile)
739
+ except BaseException:
740
+ os.remove(tryton_pass)
741
+ return -1
742
+
743
+ os.remove(tryton_pass) # Delete temp file
744
+ return 0
745
+
746
+ def refresh_instance(self, *args):
747
+ """ It updates all the modules
748
+ executting trytond-admin --all
749
+ """
750
+ win = curses.newwin(10, 50, 3, 30)
751
+ win.box()
752
+ curses.echo()
753
+ curses.curs_set(1)
754
+ win.addstr(2, 1, "Instance Name:")
755
+ dbname = win.getstr(2, 20).decode()
756
+ curses.curs_set(0)
757
+ win.addstr(4, 1, "Refreshing modules...")
758
+ win.refresh()
759
+
760
+ log = f"{self.his_base}/log/refresh_instance.log"
761
+ tadmin = f"{self.his_base}/venv/bin/trytond-admin"
762
+
763
+ """ Update the environment variables for this session
764
+ """
765
+ nenv = os.environ.copy()
766
+
767
+ with open(log, "w") as logfile:
768
+ try:
769
+ # check_call to get the return code from trytond-admin
770
+ subprocess.check_call(
771
+ [tadmin, '--database', dbname,
772
+ '--all', '-vv'],
773
+ env=nenv, stdout=logfile, stderr=logfile)
774
+ except BaseException:
775
+ attr = curses.color_pair(2) | curses.A_BOLD
776
+ win.addstr(4, 25, "[ERROR]", attr)
777
+ win.refresh()
778
+ return -1
779
+
780
+ attr = curses.color_pair(1) | curses.A_BOLD
781
+ win.addstr(4, 25, "[OK]", attr)
782
+ win.refresh()
783
+ return 0
784
+
785
+ def install_health_package(self, *args):
786
+ """ Installs the health core package
787
+ """
788
+ dbname = args[0][0]
789
+
790
+ log = f"{self.his_base}/log/install_health_package.log"
791
+ tadmin = f"{self.his_base}/venv/bin/trytond-admin"
792
+
793
+ """ Retrieve the environment variables for this session
794
+ """
795
+ nenv = os.environ.copy()
796
+
797
+ with open(log, "w") as logfile:
798
+ try:
799
+ subprocess.check_call(
800
+ [tadmin, '--database', dbname,
801
+ '--update', 'health', '--activate-dependencies', '-vv'],
802
+ env=nenv, stdout=logfile, stderr=logfile)
803
+ except BaseException:
804
+ return -1
805
+
806
+ return 0
807
+
808
+ def import_countries(self, *args):
809
+ """ Import countries
810
+ """
811
+ dbname = args[0][0]
812
+ log = f"{self.his_base}/log/import_countries.log"
813
+ imp_countries = f"{self.his_base}/venv/bin/trytond_import_countries"
814
+
815
+ """ Load environment variables for this session
816
+ """
817
+ nenv = os.environ.copy()
818
+
819
+ with open(log, "w") as logfile:
820
+ try:
821
+ subprocess.check_call(
822
+ [imp_countries, '--database', dbname],
823
+ env=nenv, stdout=logfile, stderr=logfile)
824
+ except BaseException:
825
+ return -1
826
+ return 0
827
+
828
+ def import_currencies(self, *args):
829
+ """ Import currencies
830
+ """
831
+ dbname = args[0][0]
832
+ log = f"{self.his_base}/log/import_currencies.log"
833
+ imp_currencies = f"{self.his_base}/venv/bin/trytond_import_currencies"
834
+
835
+ """ Load environment variables for this session
836
+ """
837
+ nenv = os.environ.copy()
838
+
839
+ with open(log, "w") as logfile:
840
+ try:
841
+ subprocess.check_call(
842
+ [imp_currencies, '--database', dbname],
843
+ env=nenv, stdout=logfile, stderr=logfile)
844
+ except BaseException:
845
+ return -1
846
+ return 0
847
+
848
+ def backup(self):
849
+ """ Make a backup of the database
850
+ """
851
+ timestamp = get_timestamp()
852
+
853
+ win = curses.newwin(10, 50, 3, 30)
854
+ win.box()
855
+ curses.echo()
856
+ curses.curs_set(1)
857
+ win.addstr(2, 1, "Instance Name:")
858
+ dbname = win.getstr(2, 20).decode()
859
+ curses.curs_set(0)
860
+ win.addstr(4, 1, "Backing up instance...")
861
+ win.refresh()
862
+
863
+ log = f"{self.his_base}/log/backup_instance.log"
864
+
865
+ """ Update the environment variables for this session
866
+ """
867
+ nenv = os.environ.copy()
868
+
869
+ bdest = f"{self.his_base}/backup/{dbname}-{timestamp}.sql"
870
+ with open(log, "w") as logfile:
871
+ try:
872
+ # check_call to get the return code from trytond-admin
873
+ subprocess.check_call(
874
+ ['pg_dump', '--dbname', dbname,
875
+ '--file', bdest],
876
+ env=nenv, stdout=logfile, stderr=logfile)
877
+ except BaseException:
878
+ attr = curses.color_pair(2) | curses.A_BOLD
879
+ win.addstr(4, 25, "[ERROR]", attr)
880
+ win.refresh()
881
+ return -1
882
+
883
+ attr = curses.color_pair(1) | curses.A_BOLD
884
+ win.addstr(4, 25, "[OK]", attr)
885
+ win.refresh()
886
+ return 0
887
+
888
+ def command_successful(self, *args):
889
+ return 0
890
+
891
+ def new(self, dbname=None):
892
+ """ Get the instance/db name, admin password and email
893
+ """
894
+ win = curses.newwin(10, 50, 3, 30)
895
+ win.box()
896
+ curses.echo()
897
+ curses.curs_set(1)
898
+ win.addstr(2, 1, "Instance Name:")
899
+ db = win.getstr(2, 20).decode()
900
+ win.addstr(3, 1, "Password:")
901
+ password = win.getstr(3, 20).decode()
902
+ win.addstr(4, 1, "email:")
903
+ email = win.getstr(4, 20).decode()
904
+ win.addstr(6, 1, "Ready? (yes/no)")
905
+ confirm = win.getstr(6, 20).decode()
906
+ if (confirm != "yes"):
907
+ return
908
+
909
+ """
910
+ If we answer yes, go ahead with the instance creation
911
+ """
912
+ curses.curs_set(0)
913
+ win.clear()
914
+ win.refresh()
915
+ logwin = log_window()
916
+ logwin.refresh()
917
+
918
+ """ TASKS is an list of tuples, each element containing
919
+ the method and it's arguments
920
+ """
921
+ TASKS = [('create_db', [db]),
922
+ ('create_instance', [db, email, password]),
923
+ ('install_health_package', [db]),
924
+ ('import_countries', [db]),
925
+ ('import_currencies', [db]),
926
+ ('command_successful', []),
927
+ ]
928
+ row = 0
929
+ for task in TASKS:
930
+ row = row + 1
931
+ tsk = getattr(self, task[0])
932
+ # Pass the string and the actual method as arguments
933
+ rc = task_engine(task=(task, tsk), window=logwin, row=row)
934
+ if rc != 0:
935
+ break
936
+
937
+
938
+ class Help:
939
+
940
+ def display():
941
+ """ show the help window
942
+ """
943
+ pass
944
+
945
+
946
+ class MenuPanel:
947
+ def __init__(self, entries, win, footer, title, leaf=False, action=None):
948
+ curses.start_color()
949
+ # Init color pairs
950
+ curses.init_pair(1, curses.COLOR_GREEN, curses.COLOR_BLACK) # OK
951
+ curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_RED) # ERROR
952
+ curses.init_pair(3, curses.COLOR_YELLOW, curses.COLOR_BLACK) # WARNING
953
+ self.window = win
954
+ self.window_title = title
955
+ self.panel = panel.new_panel(self.window)
956
+ self.panel.hide()
957
+ self.footer = footer
958
+ self.footer_panel = panel.new_panel(self.footer)
959
+ self.leaf = leaf
960
+ self.action = action
961
+ panel.update_panels()
962
+
963
+ self.position = 0
964
+ self.entries = entries
965
+
966
+ def centerx(self, msg):
967
+ x = int(curses.COLS / 2) - int(len(msg) / 2)
968
+ return x
969
+
970
+ def menu(self):
971
+ """ Show the menu of the specific section
972
+ """
973
+ self.window.clear()
974
+ self.window.addstr(
975
+ 0, self.centerx(self.window_title), self.window_title)
976
+ self.panel.top()
977
+ self.panel.show()
978
+
979
+ """ Main menu loop"""
980
+ while True:
981
+ self.window.refresh()
982
+ curses.doupdate()
983
+ for index, entry in enumerate(self.entries):
984
+ if index == self.position:
985
+ mode = curses.A_REVERSE
986
+ else:
987
+ mode = curses.A_NORMAL
988
+
989
+ entry_desc = f"{index}: {entry[0]}"
990
+ self.window.addstr(5 + index, 2, entry_desc, mode)
991
+
992
+ self.window.hline(1, 0, curses.ACS_HLINE, curses.COLS)
993
+
994
+ key = self.window.getch()
995
+
996
+ if key == ord("\n"): # Map the Enter key
997
+ # Exec the method associated to the entry
998
+ if not self.leaf:
999
+ if self.entries[self.position][1]:
1000
+ self.entries[self.position][1]()
1001
+ else:
1002
+ self.entries[self.position][1]()
1003
+
1004
+ if key == ord("q"):
1005
+ break
1006
+
1007
+ if key == ord("h"):
1008
+ Help.display()
1009
+
1010
+ if key == curses.KEY_UP and self.position > 0:
1011
+ self.position = self.position - 1
1012
+
1013
+ if (key == curses.KEY_DOWN
1014
+ and self.position < len(self.entries) - 1):
1015
+ self.position = self.position + 1
1016
+
1017
+ if chr(key).isnumeric():
1018
+ entry_nr = int(chr(key))
1019
+ if entry_nr in range(0, len(self.entries)):
1020
+ self.position = int(chr(key))
1021
+ else:
1022
+ # Out of range
1023
+ self.menu_error()
1024
+
1025
+ self.window.clear()
1026
+ self.panel.hide()
1027
+ panel.update_panels()
1028
+ curses.doupdate()
1029
+
1030
+ def menu_error(self):
1031
+ curses.beep()
1032
+
1033
+
1034
+ class GHControl:
1035
+ def __init__(self, stdscr, arguments):
1036
+ curses.curs_set(False) # Disable cursor
1037
+ basedir = arguments.basedir
1038
+
1039
+ self.win = self.main_window(stdscr)
1040
+ self.footer = self.footer_window(stdscr)
1041
+ server = Server(self, arguments=arguments)
1042
+ instance = Instance(self, arguments=arguments)
1043
+ installation_entries = [
1044
+ ("Start Installation", server.setup, arguments),]
1045
+ installation_section = MenuPanel(
1046
+ installation_entries, self.win, self.footer,
1047
+ f"GNU Health HIS Installation (basedir = {basedir})", leaf=True)
1048
+
1049
+ update_entries = [
1050
+ ("Start Packages Update", server.update, arguments),]
1051
+ update_section = MenuPanel(
1052
+ update_entries, self.win, self.footer,
1053
+ "GNU Health HIS Packages Update", leaf=True)
1054
+
1055
+ package_entries = [
1056
+ ("Add Health package", server.add_package, arguments),]
1057
+ package_section = MenuPanel(
1058
+ package_entries, self.win, self.footer,
1059
+ "GNU Health HIS Add Package", leaf=True)
1060
+
1061
+ instance_entries = [
1062
+ ("New Instance", instance.new, arguments),]
1063
+ instance_section = MenuPanel(
1064
+ instance_entries, self.win, self.footer,
1065
+ "GNU Health Instance", leaf=True)
1066
+
1067
+ refresh_entries = [
1068
+ ("Refresh Instance", instance.refresh_instance),]
1069
+ refresh_section = MenuPanel(
1070
+ refresh_entries, self.win, self.footer,
1071
+ "GNU Health Instance", leaf=True)
1072
+
1073
+ backup_entries = [("DB Instance Backup", instance.backup),]
1074
+ backup_section = MenuPanel(
1075
+ backup_entries, self.win, self.footer, "Backup", leaf=True)
1076
+
1077
+ startstop_entries = [
1078
+ ("Start GNU Health HIS", server.start),
1079
+ ("Stop Server", server.stop),
1080
+ ]
1081
+ startstop_section = MenuPanel(
1082
+ startstop_entries, self.win, self.footer,
1083
+ "Server start / stop", leaf=True)
1084
+
1085
+ main_entries = [
1086
+ ("Install GNU Health HIS", installation_section.menu),
1087
+ ("Create a new DB instance", instance_section.menu),
1088
+ ("Add Health package", package_section.menu),
1089
+ ("Refresh DB instance", refresh_section.menu),
1090
+ ("Start / stop instance", startstop_section.menu),
1091
+ ("Update packages / dependencies", update_section.menu),
1092
+ ("Backup instance", backup_section.menu),
1093
+ ]
1094
+
1095
+ main_section = MenuPanel(
1096
+ main_entries, self.win, self.footer,
1097
+ f"Welcome to GNU Health Control Center {VERSION}")
1098
+
1099
+ main_section.menu()
1100
+
1101
+ def main_window(self, stdscr):
1102
+ """ We'll define a main window
1103
+ """
1104
+ win = curses.newwin(curses.LINES - 5, curses.COLS, 0, 0)
1105
+ win.keypad(True) # Need keypad to map KEY_[UP|DOWN]
1106
+ return win
1107
+
1108
+ def footer_window(self, stdscr):
1109
+ """ A footer for helpers
1110
+ """
1111
+ footer = curses.newwin(5, curses.COLS, curses.LINES - 5, 0)
1112
+ footer.box()
1113
+ server_status = Server.status(self)
1114
+
1115
+ if server_status:
1116
+ attr = curses.color_pair(1) | curses.A_BOLD
1117
+ status = "running"
1118
+ else:
1119
+ attr = curses.color_pair(3) | curses.A_BOLD
1120
+ status = "stopped"
1121
+
1122
+ footer_status = "Server status:"
1123
+ footer_hlp = "Press 'q' to go back or exit"
1124
+ footer.addstr(
1125
+ 1, int(curses.COLS / 2 - len(footer_status + status) / 2),
1126
+ footer_status)
1127
+
1128
+ footer.addstr(
1129
+ 1,
1130
+ int(
1131
+ curses.COLS / 2 - len(footer_status + status) / 2)
1132
+ + len(footer_status) + 1, status, attr)
1133
+
1134
+ footer.addstr(3, int(curses.COLS / 2 - len(footer_hlp) / 2),
1135
+ footer_hlp)
1136
+
1137
+ footer.refresh()
1138
+ return footer
1139
+
1140
+
1141
+ def cmdline_args():
1142
+ parser = argparse.ArgumentParser()
1143
+
1144
+ parser.add_argument('-b', '--basedir', default='/opt/gnuhealth',
1145
+ help="Base directory of installation for "
1146
+ "the gnuhealth components\n"
1147
+ "default=/opt/gnuhealth.")
1148
+
1149
+ parser.add_argument('-r', '--release',
1150
+ help="GNU Health release (major and minor numbers), "
1151
+ "It will affect the installation directory of his, "
1152
+ "for example: his-50.")
1153
+
1154
+ parser.add_argument('-i', '--ignore-bashrc', action="store_true",
1155
+ help="Does not modify .bashrc, "
1156
+ "If users want to install different HIS versions. "
1157
+ "If used, the user should use '/path/to/ghis_env'\n "
1158
+ "For example: "
1159
+ "'/opt/gnuhealth/his-50/ghis_env trytond', "
1160
+ "'/opt/gnuhealth/his-50/ghis_env trytond-admin', "
1161
+ "'/opt/gnuhealth/his-50/ghis_env pip install pandas'")
1162
+
1163
+ parser.add_argument('-u', '--update-config', action="store_true",
1164
+ help="Updates configuration files, namely "
1165
+ "tryton.cfg, gnuhealthrc and bashrc, during "
1166
+ "the server installation process.")
1167
+
1168
+ parser.add_argument('-t', '--test', action="store_true",
1169
+ help="Test mode. Uses the repository "
1170
+ "test.pypi.org ")
1171
+
1172
+ parser.add_argument('-p', '--pre', action="store_true",
1173
+ help="Use pre-release packages. Usually used in "
1174
+ "combination with --test")
1175
+
1176
+ return parser.parse_args()
1177
+
1178
+
1179
+ def main():
1180
+ arguments = cmdline_args()
1181
+ wrapper(GHControl, arguments)
1182
+
1183
+
1184
+ if __name__ == "__main__":
1185
+ main()