pax_global_header 0000666 0000000 0000000 00000000064 14012562772 0014520 g ustar 00root root 0000000 0000000 52 comment=0af8b5111c7d4471c4d9a45dbbb9c57376d435b8
python-hl7-0.4.2/ 0000775 0000000 0000000 00000000000 14012562772 0013534 5 ustar 00root root 0000000 0000000 python-hl7-0.4.2/.coveragerc 0000664 0000000 0000000 00000000045 14012562772 0015654 0 ustar 00root root 0000000 0000000 [run]
branch = True
omit =
env/* python-hl7-0.4.2/.flake8 0000664 0000000 0000000 00000000415 14012562772 0014707 0 ustar 00root root 0000000 0000000 # -*- mode: conf; -*-
[flake8]
# Recommend matching the black line length (default 88),
# rather than using the flake8 default of 79:
max-line-length = 88
max-complexity = 10
ignore = E203, E501, W503
exclude =
.git,
env,
__pycache__,
build,
dist python-hl7-0.4.2/.github/ 0000775 0000000 0000000 00000000000 14012562772 0015074 5 ustar 00root root 0000000 0000000 python-hl7-0.4.2/.github/workflows/ 0000775 0000000 0000000 00000000000 14012562772 0017131 5 ustar 00root root 0000000 0000000 python-hl7-0.4.2/.github/workflows/codeql-analysis.yml 0000664 0000000 0000000 00000005032 14012562772 0022744 0 ustar 00root root 0000000 0000000 # For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
name: "CodeQL"
on:
push:
branches: [master]
pull_request:
# The branches below must be a subset of the branches above
branches: [master]
schedule:
- cron: '0 16 * * 1'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# Override automatic language detection by changing the below list
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
language: ['python']
# Learn more...
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
python-hl7-0.4.2/.github/workflows/test.yaml 0000664 0000000 0000000 00000003075 14012562772 0021001 0 ustar 00root root 0000000 0000000 name: Python package
on: [push, pull_request]
env:
primary-python-version: 3.8
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.5, 3.6, 3.7, 3.8, 3.9]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip virtualenv
make env
# - name: Lint with flake8
# run: |
# pip install flake8
# # stop the build if there are Python syntax errors or undefined names
# flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
# flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test
run: |
make coverage
- name: Check code formatting
if: matrix.python-version == env.primary-python-version
# Includes flake8, black --check, and isort --check-only
run: |
make lint
- name: Upload to codecov.io
# Only upload coverage report for single version
if: matrix.python-version == env.primary-python-version
uses: codecov/codecov-action@v1.0.5
with:
token: ${{ secrets.CODECOV_TOKEN }}
- name: Docs and Doctests
# Only check docs on single version
if: matrix.python-version >= env.primary-python-version
run: |
make docs
python-hl7-0.4.2/.gitignore 0000664 0000000 0000000 00000000161 14012562772 0015522 0 ustar 00root root 0000000 0000000 *\~
*#*
*.pyc
*.egg-info/
*.egg
*.mypy_cache/
.coverage
coverage.xml
/.tox/
/build/
/dist/
/env/
/.eggs/
.vscode
python-hl7-0.4.2/.hgignore 0000664 0000000 0000000 00000000064 14012562772 0015337 0 ustar 00root root 0000000 0000000 syntax: glob
*.pyc
*.egg-info
*\#*
*~*
build/
dist/
python-hl7-0.4.2/AUTHORS 0000664 0000000 0000000 00000000336 14012562772 0014606 0 ustar 00root root 0000000 0000000 * `John Paulett `_ (john -at- paulett.org)
* `Andrew Wason `_
* `Kevin Gill `_
* `Emilien Klein `_
python-hl7-0.4.2/LICENSE 0000664 0000000 0000000 00000002634 14012562772 0014546 0 ustar 00root root 0000000 0000000 Copyright (C) 2009-2020 John Paulett (john -at- paulett.org)
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
3. The name of the author may not be used to endorse or promote
products derived from this software without specific prior
written permission.
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
python-hl7-0.4.2/MANIFEST.in 0000664 0000000 0000000 00000000160 14012562772 0015267 0 ustar 00root root 0000000 0000000 include LICENSE README.rst
include *.py
include tests/*.py
include docs/**
exclude docs/_build
global-exclude *~ python-hl7-0.4.2/Makefile 0000664 0000000 0000000 00000002321 14012562772 0015172 0 ustar 00root root 0000000 0000000 .PHONY: test tests build docs lint upload
BIN = env/bin
PYTHON = $(BIN)/python
PIP = $(BIN)/pip
SPHINXBUILD = $(shell pwd)/env/bin/sphinx-build
env: requirements.txt setup.py
test -f $(PYTHON) || virtualenv env
$(PIP) install -U -r requirements.txt
$(PYTHON) setup.py develop
tests: env
$(BIN)/tox
.PHONY: tests
# Alias for old-style invocation
test: tests
.PHONY: test
coverage:
$(BIN)/coverage run -m unittest discover -t . -s tests
$(BIN)/coverage xml
.PHONY: coverage
build:
$(PYTHON) setup.py sdist
.PHONY: build
clean-docs:
cd docs; make clean
.PHONY: clean-docs
clean: clean-docs
rm -rf *.egg-info .mypy_cache coverage.xml env
find . -name "*.pyc" -type f -delete
find . -type d -empty -delete
.PHONY: clean-python
docs:
cd docs; make html SPHINXBUILD=$(SPHINXBUILD); make man SPHINXBUILD=$(SPHINXBUILD); make doctest SPHINXBUILD=$(SPHINXBUILD)
lint:
$(BIN)/flake8 hl7 tests
CHECK_ONLY=true $(MAKE) format
.PHONY: lint
CHECK_ONLY ?=
ifdef CHECK_ONLY
ISORT_ARGS=--check-only
BLACK_ARGS=--check
endif
format:
$(BIN)/isort -rc $(ISORT_ARGS) hl7 tests
$(BIN)/black $(BLACK_ARGS) hl7 tests
.PHONY: isort
upload:
rm -rf dist
$(PYTHON) setup.py sdist bdist_wheel
twine upload dist/*
.PHONY: upload
python-hl7-0.4.2/README.rst 0000664 0000000 0000000 00000001161 14012562772 0015222 0 ustar 00root root 0000000 0000000 python-hl7 - HL7 2.x Parsing
============================
python-hl7 is a simple library for parsing messages of Health Level 7
(HL7) version 2.x into Python objects.
* Source Code: http://github.com/johnpaulett/python-hl7
* Documentation: http://python-hl7.readthedocs.org
* PyPi: http://pypi.python.org/pypi/hl7
.. image::
https://github.com/johnpaulett/python-hl7/workflows/Python%20package/badge.svg
:target: https://github.com/johnpaulett/python-hl7/actions
.. warning::
python-hl7 v0.3.0 breaks `backwards compatibility
`_.
python-hl7-0.4.2/docs/ 0000775 0000000 0000000 00000000000 14012562772 0014464 5 ustar 00root root 0000000 0000000 python-hl7-0.4.2/docs/.gitignore 0000664 0000000 0000000 00000000011 14012562772 0016444 0 ustar 00root root 0000000 0000000 /_build/
python-hl7-0.4.2/docs/Makefile 0000664 0000000 0000000 00000010776 14012562772 0016137 0 ustar 00root root 0000000 0000000 # Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest
help:
@echo "Please use \`make ' where is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
-rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/python-hl7.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/python-hl7.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/python-hl7"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/python-hl7"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
make -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
python-hl7-0.4.2/docs/_static/ 0000775 0000000 0000000 00000000000 14012562772 0016112 5 ustar 00root root 0000000 0000000 python-hl7-0.4.2/docs/_static/.gitignore 0000664 0000000 0000000 00000000000 14012562772 0020070 0 ustar 00root root 0000000 0000000 python-hl7-0.4.2/docs/_templates/ 0000775 0000000 0000000 00000000000 14012562772 0016621 5 ustar 00root root 0000000 0000000 python-hl7-0.4.2/docs/_templates/.gitignore 0000664 0000000 0000000 00000000000 14012562772 0020577 0 ustar 00root root 0000000 0000000 python-hl7-0.4.2/docs/accessors.rst 0000664 0000000 0000000 00000021341 14012562772 0017204 0 ustar 00root root 0000000 0000000 Message Accessor
================
Reproduced from: http://wiki.medical-objects.com.au/index.php/Hl7v2_parsing
.. note::
Warning: Indexes in this API are from 1, not 0. This is to align with the HL7 documentation.
Example HL7 Fragment:
.. doctest::
>>> message = 'MSH|^~\&|\r'
>>> message += 'PID|Field1|Component1^Component2|Component1^Sub-Component1&Sub-Component2^Component3|Repeat1~Repeat2\r\r'
>>> import hl7
>>> h = hl7.parse(message)
The resulting parse tree with values in quotes:
| Segment = "PID"
| F1
| R1 = "Field1"
| F2
| R1
| C1 = "Component1"
| C2 = "Component2"
| F3
| R1
| C1 = "Component1"
| C2
| S1 = "Sub-Component1"
| S2 = "Sub-Component2"
| C3 = "Component3"
| F4
| R1 = "Repeat1"
| R2 = "Repeat2"
| Legend
|
| F Field
| R Repeat
| C Component
| S Sub-Component
A tree has leaf values and nodes. Only the leaves of the tree can have a value.
All data items in the message will be in a leaf node.
After parsing, the data items in the message are in position in the parse tree, but
they remain in their escaped form. To extract a value from the tree you start at the
root of the Segment and specify the details of which field value you want to extract.
The minimum specification is the field number and repeat number. If you are after a
component or sub-component value you also have to specify these values.
If for instance if you want to read the value "Sub-Component2" from the example HL7
you need to specify: Field 3, Repeat 1, Component 2, Sub-Component 2 (PID.F1.R1.C2.S2).
Reading values from a tree structure in this manner is the only safe way to read data
from a message.
.. doctest::
>>> h['PID.F1.R1']
'Field1'
>>> h['PID.F2.R1.C1']
'Component1'
You can also access values using :py:class:`hl7.Accessor`, or by directly calling
:py:meth:`hl7.Message.extract_field`. The following are all equivalent:
.. doctest::
>>> h['PID.F2.R1.C1']
'Component1'
>>> h[hl7.Accessor('PID', 1, 2, 1, 1)]
'Component1'
>>> h.extract_field('PID', 1, 2, 1, 1)
'Component1'
All values should be accessed in this manner. Even if a field is marked as being
non-repeating a repeat of "1" should be specified as later version messages
could have a repeating value.
To enable backward and forward compatibility there are rules for reading values when the
tree does not match the specification (eg PID.F1.R1.C2.S2) The common example of this is
expanding a HL7 "IS" Value into a Codeded Value ("CE"). Systems reading a "IS" value would
read the Identifier field of a message with a "CE" value and systems expecting a "CE" value
would see a Coded Value with only the identifier specified. A common Australian example of
this is the OBX Units field, which was an "IS" value previously and became a "CE" Value
in later versions.
| Old Version: "\|mmol/l\|" New Version: "\|mmol/l^^ISO+\|"
Systems expecting a simple "IS" value would read "OBX.F6.R1" and this would yield a value
in the tree for an old message but with a message with a Coded Value that tree node would
not have a value, but would have 3 child Components with the "mmol/l" value in the first
subcomponent. To resolve this issue where the tree is deeper than the specified path the
first node of every child node is traversed until a leaf node is found and that value is
returned.
.. doctest::
>>> h['PID.F3.R1.C2']
'Sub-Component1'
This is a general rule for reading values: **If the parse tree is deeper than the specified
path continue following the first child branch until a leaf of the tree is encountered
and return that value (which could be blank).**
Systems expecting a Coded Value ("CE"), but reading a message with a simple "IS" value in it
have the opposite problem. They have a deeper specification but have reached a leaf node and
cannot follow the path any further. Reading a "CE" value requires multiple reads for each
sub-component but for the "Identifier" in this example the specification would be "OBX.F6.R1.C1".
The tree would stop at R1 so C1 would not exist. In this case the unsatisfied path elements
(C1 in this case) can be examined and if every one is position 1 then they can be ignored and
the leaf of the tree that was reached returned. If any of the unsatisfied paths are not in
position 1 then this cannot be done and the result is a blank string.
This is the second Rule for reading values: **If the parse tree terminates before the full path
is satisfied check each of the subsequent paths and if every one is specified at position 1
then the leaf value reached can be returned as the result.**
.. doctest::
>>> h['PID.F1.R1.C1.S1']
'Field1'
This is a general rule for reading values: **If the parse tree is deeper than the specified
path continue following the first child branch until a leaf of the tree is encountered
and return that value (which could be blank).**
In the second example every value that makes up the Coded Value, other than the identifier
has a component position greater than one and when reading a message with a simple "IS"
value in it, every value other than the identifier would return a blank string.
Following these rules will result in excellent backward and forward compatibility. It is
important to allow the reading of values that do not exist in the parse tree by simply
returning a blank string. The two rules detailed above, along with the full tree specification
for all values being read from a message will eliminate many of the errors seen when
handling earlier and later message versions.
.. doctest::
>>> h['PID.F10.R1']
''
At this point the desired value has either been located, or is absent, in which case a blank
string is returned.
Assignments
-----------
The accessors also support item assignments. However, the Message object must exist and the
separators must be validly assigned.
Create a response message.
.. doctest::
>>> SEP = '|^~\&'
>>> CR_SEP = '\r'
>>> MSH = hl7.Segment(SEP[0], [hl7.Field(SEP[1], ['MSH'])])
>>> MSA = hl7.Segment(SEP[0], [hl7.Field(SEP[1], ['MSA'])])
>>> response = hl7.Message(CR_SEP, [MSH, MSA])
>>> response['MSH.F1.R1'] = SEP[0]
>>> response['MSH.F2.R1'] = SEP[1:]
>>> str(response)
'MSH|^~\\&|\rMSA\r'
Assign values into the message. You can only assign a string into the message (i.e. a leaf
of the tree).
.. doctest::
>>> response['MSH.F9.R1.C1'] = 'ORU'
>>> response['MSH.F9.R1.C2'] = 'R01'
>>> response['MSH.F9.R1.C3'] = ''
>>> response['MSH.F12.R1'] = '2.4'
>>> response['MSA.F1.R1'] = 'AA'
>>> response['MSA.F3.R1'] = 'Application Message'
>>> str(response)
'MSH|^~\\&|||||||ORU^R01^|||2.4\rMSA|AA||Application Message\r'
You can also assign values using :py:class:`hl7.Accessor`, or by directly calling
:py:meth:`hl7.Message.assign_field`. The following are all equivalent:
.. doctest::
>>> response['MSA.F1.R1'] = 'AA'
>>> response[hl7.Accessor('MSA', 1, 1, 1)] = 'AA'
>>> response.assign_field('AA', 'MSA', 1, 1, 1)
Escaping Content
----------------
HL7 messages are transported using the 7bit ascii character set. Only characters between
ascii 32 and 127 are used. Characters which cannot be transported using this range
of values must be 'escaped', that is replaced by a sequence of characters for transmission.
The stores values internally in the escaped format. When the message is composed using
'str', the escaped value must be returned.
.. doctest::
>>> message = 'MSH|^~\&|\r'
>>> message += 'PID|Field1|\F\|\r\r'
>>> h = hl7.parse(message)
>>> str(h['PID'][0][2])
'\\F\\'
>>> h.unescape(str(h['PID'][0][2]))
'|'
When the accessor is used to reference the field, the field is automatically unescaped.
.. doctest::
>>> h['PID.F2.R1']
'|'
The escape/unescape mechanism support replacing separator characters with their escaped
version and replacing non-ascii characters with hexadecimal versions.
The escape method returns a 'str' object. The unescape method returns a str object.
.. doctest::
>>> h.unescape('\\F\\')
'|'
>>> h.unescape('\\R\\')
'~'
>>> h.unescape('\\S\\')
'^'
>>> h.unescape('\\T\\')
'&'
>>> h.unescape('\\X202020\\')
' '
>>> h.escape('|~^&')
'\\F\\\\R\\\\S\\\\T\\'
>>> h.escape('áéíóú')
'\\Xe1\\\\Xe9\\\\Xed\\\\Xf3\\\\Xfa\\'
**Presentation Characters**
HL7 defines a protocol for encoding presentation characters, These include hightlighting,
and rich text functionality. The API does not currently allow for easy access to the
escape/unescape logic. You must overwrite the message class escape and unescape methods,
after parsing the message.
python-hl7-0.4.2/docs/api.rst 0000664 0000000 0000000 00000004776 14012562772 0016005 0 ustar 00root root 0000000 0000000 python-hl7 API
==============
.. testsetup:: *
import hl7
message = 'MSH|^~\&|GHH LAB|ELAB-3|GHH OE|BLDG4|200202150930||ORU^R01|CNTRL-3456|P|2.4\r'
message += 'PID|||555-44-4444||EVERYWOMAN^EVE^E^^^^L|JONES|196203520|F|||153 FERNWOOD DR.^^STATESVILLE^OH^35292||(206)3345232|(206)752-121||||AC555444444||67-A4335^OH^20030520\r'
message += 'OBR|1|845439^GHH OE|1045813^GHH LAB|1554-5^GLUCOSE|||200202150730||||||||555-55-5555^PRIMARY^PATRICIA P^^^^MD^^LEVEL SEVEN HEALTHCARE, INC.|||||||||F||||||444-44-4444^HIPPOCRATES^HOWARD H^^^^MD\r'
message += 'OBX|1|SN|1554-5^GLUCOSE^POST 12H CFST:MCNC:PT:SER/PLAS:QN||^182|mg/dl|70_105|H|||F\r'
.. autodata:: hl7.NULL
.. autofunction:: hl7.parse
.. autofunction:: hl7.parse_batch
.. autofunction:: hl7.parse_file
.. autofunction:: hl7.parse_hl7
.. autofunction:: hl7.ishl7
.. autofunction:: hl7.isbatch
.. autofunction:: hl7.isfile
.. autofunction:: hl7.split_file
.. autofunction:: hl7.generate_message_control_id
.. autofunction:: hl7.parse_datetime
Data Types
----------
.. autoclass:: hl7.Sequence
:members: __call__
.. autoclass:: hl7.Container
:members: __str__
.. autoclass:: hl7.Accessor
:members: __new__, parse_key, key, _replace, _make, _asdict, segment, segment_num, field_num, repeat_num, component_num, subcomponent_num
.. autoclass:: hl7.Batch
:members: __str__, header, trailer, create_header, create_trailer, create_file, create_batch, create_message, create_segment, create_field, create_repetition, create_component
.. autoclass:: hl7.File
:members: __str__, header, trailer, create_header, create_trailer, create_file, create_batch, create_message, create_segment, create_field, create_repetition, create_component
.. autoclass:: hl7.Message
:members: segments, segment, __getitem__, __setitem__, __str__, escape, unescape, extract_field, assign_field, create_file, create_batch, create_message, create_segment, create_field, create_repetition, create_component, create_ack
.. autoclass:: hl7.Segment
.. autoclass:: hl7.Field
.. autoclass:: hl7.Repetition
.. autoclass:: hl7.Component
.. autoclass:: hl7.Factory
:members:
MLLP Network Client
-------------------
.. autoclass:: hl7.client.MLLPClient
:members: send_message, send, close
MLLP Asyncio
------------
.. autofunction:: hl7.mllp.open_hl7_connection
.. autofunction:: hl7.mllp.start_hl7_server
.. autoclass:: hl7.mllp.HL7StreamReader
:members: readmessage
.. autoclass:: hl7.mllp.HL7StreamWriter
:members: writemessage
.. autoclass:: hl7.mllp.InvalidBlockError
python-hl7-0.4.2/docs/authors.rst 0000664 0000000 0000000 00000000052 14012562772 0016700 0 ustar 00root root 0000000 0000000 Authors
=======
.. include:: ../AUTHORS
python-hl7-0.4.2/docs/changelog.rst 0000664 0000000 0000000 00000013262 14012562772 0017151 0 ustar 00root root 0000000 0000000 Changelog
=========
0.4.2 - February 2021
---------------------
* Added support for :py:class:`hl7.Batch` and :py:class:`hl7.File`, via
:py:func:`hl7.parse_hl7` or the more specific :py:func:`hl7.parse_batch`
and :py:func:`parse_file`.
Thanks `Joseph Wortmann `_!
0.4.1 - September 2020
----------------------
* Experimental asyncio-based HL7 MLLP support. :doc:`mllp`, via
:py:func:`hl7.mllp.open_hl7_connection` and
:py:func:`hl7.mllp.start_hl7_server`
Thanks `Joseph Wortmann `_!
.. _changelog-0-4-0:
0.4.0 - September 2020
----------------------
* Message now ends with trailing carriage return, to be consistent with Message
Construction Rules (Section 2.6, v2.8). [`python-hl7#26 `]
* Handle ASCII characters within :py:meth:`hl7.Message.escape` under Python 3.
* Don't escape MSH-2 so that the control characters are retrievable. [`python-hl7#27 `]
* Add MSH-9.1.3 to create_ack.
* Dropped support for Python 2.7, 3.3, & 3.4. Python 3.5 - 3.8 now supported.
* Converted code style to use black.
Thanks `Lucas Kahlert `_ &
`Joseph Wortmann `_!
0.3.5 - June 2020
-----------------
* Handle ASCII characters within :py:meth:`hl7.Message.escape` under Python 3.
Thanks `Lucas Kahlert `_!
0.3.4 - June 2016
-----------------
* Fix bug under Python 3 when writing to stdout from `mllp_send`
* Publish as a Python wheel
0.3.3 - June 2015
-----------------
* Expose a Factory that allows control over the container subclasses created
to construct a message
* Split up single module into more manageable submodules.
Thanks `Andrew Wason `_!
0.3.2 - September 2014
----------------------
* New :py:func:`hl7.parse_datetime` for parsing HL7 DTM into python
:py:class:`datetime.datetime`.
0.3.1 - August 2014
-------------------
* Allow HL7 ACK's to be generated from an existing Message via
:py:meth:`hl7.Message.create_ack`
.. _changelog-0-3-0:
0.3.0 - August 2014
-------------------
.. warning::
:ref:`0.3.0 ` breaks backwards compatibility by correcting
the indexing of the MSH segment and the introducing improved parsing down to
the repetition and sub-component level.
* Changed the numbering of fields in the MSH segment.
**This breaks older code.**
* Parse all the elements of the message (i.e. down to sub-component). **The
inclusion of repetitions will break older code.**
* Implemented a basic escaping mechanism
* New constant 'NULL' which maps to '""'
* New :py:func:`hl7.isfile` and :py:func:`hl7.split_file` functions to
identify file (FHS/FTS) wrapped messages
* New mechanism to address message parts via a :doc:`symbolic accessor name
`
* Message (and Message.segments), Field, Repetition and Component can be
accessed using 1-based indices by using them as a callable.
* Added Python 3 support. Python 2.6, 2.7, and 3.3 are officially supported.
* :py:func:`hl7.parse` can now decode byte strings, using the ``encoding``
parameter. :py:class:`hl7.client.MLLPClient` can now encode unicode input
using the ``encoding`` parameter. To support Python 3, unicode is now
the primary string type used inside the library. bytestrings are only
allowed at the edge of the library now, with ``hl7.parse`` and sending
via ``hl7.client.MLLPClient``. Refer to :ref:`unicode-vs-byte-strings`.
* Testing via tox and travis CI added. See :doc:`contribute`.
A massive thanks to `Kevin Gill `_ and
`Emilien Klein `_ for the initial code submissions
to add the improved parsing, and to
`Andrew Wason `_ for rebasing the initial pull
request and providing assistance in the transition.
0.2.5 - March 2012
------------------
* Do not senselessly try to convert to unicode in mllp_send. Allows files to
contain other encodings.
0.2.4 - February 2012
---------------------
* ``mllp_send --version`` prints version number
* ``mllp_send --loose`` algorithm modified to allow multiple messages per file.
The algorithm now splits messages based upon the presumed start of a message,
which must start with ``MSH|^~\&|``
0.2.3 - January 2012
--------------------
* ``mllp_send --loose`` accepts & converts Unix newlines in addition to
Windows newlines
0.2.2 - December 2011
---------------------
* :ref:`mllp_send ` now takes the ``--loose`` options, which allows
sending HL7 messages that may not exactly meet the standard (Windows newlines
separating segments instead of carriage returns).
0.2.1 - August 2011
-------------------
* Added MLLP client (:py:class:`hl7.client.MLLPClient`) and command line tool,
:ref:`mllp_send `.
0.2.0 - June 2011
-----------------
* Converted ``hl7.segment`` and ``hl7.segments`` into methods on
:py:class:`hl7.Message`.
* Support dict-syntax for getting Segments from a Message (e.g. ``message['OBX']``)
* Use unicode throughout python-hl7 since the HL7 spec allows non-ASCII characters.
It is up to the caller of :py:func:`hl7.parse` to convert non-ASCII messages
into unicode.
* Refactored from single hl7.py file into the hl7 module.
* Added Sphinx `documentation `_.
Moved project to `github `_.
0.1.1 - June 2009
-----------------
* Apply Python 3 trove classifier
0.1.0 - March 2009
------------------
* Support message-defined separation characters
* Message, Segment, Field classes
0.0.3 - January 2009
--------------------
* Initial release
python-hl7-0.4.2/docs/conf.py 0000664 0000000 0000000 00000020723 14012562772 0015767 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
#
# python-hl7 documentation build configuration file, created by
# sphinx-quickstart on Tue Jul 12 10:57:30 2011.
#
# This file is execfile()d with the current directory set to its containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys, os
from datetime import date
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.insert(0, os.path.abspath(".."))
import hl7
# -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ["sphinx.ext.autodoc", "sphinx.ext.doctest", "sphinx.ext.intersphinx"]
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
# The suffix of source filenames.
source_suffix = ".rst"
# The encoding of source files.
# source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = "index"
# General information about the project.
project = u"python-hl7"
copyright = u"2011-{}, John Paulett".format(date.today().year)
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = hl7.__version__
# The full version, including alpha/beta/rc tags.
release = hl7.__version__
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
# language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
# today = ''
# Else, today_fmt is used as the format for a strftime call.
# today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ["_build"]
# The reST default role (used for this markup: `text`) to use for all documents.
# default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
# add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
# add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = "sphinx"
# A list of ignored prefixes for module index sorting.
# modindex_common_prefix = []
# -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = "alabaster"
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
html_theme_options = {
"description": "Easy HL7 v2.x parsing",
"github_user": "johnpaulett",
"github_repo": "python-hl7",
"codecov_button": True,
"github_banner": True,
# "page_width": "940",
}
# Add any paths that contain custom themes here, relative to this directory.
# html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# " v documentation".
# html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
# html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
# html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
# html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["_static"]
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
# html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
# html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
# html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
# html_additional_pages = {}
# If false, no module index is generated.
# html_domain_indices = True
# If false, no index is generated.
# html_use_index = True
# If true, the index is split into individual pages for each letter.
# html_split_index = False
# If true, links to the reST sources are added to the pages.
# html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
# html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
# html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
# html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
# html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = "python-hl7doc"
# -- Options for LaTeX output --------------------------------------------------
# The paper size ('letter' or 'a4').
# latex_paper_size = 'letter'
# The font size ('10pt', '11pt' or '12pt').
# latex_font_size = '10pt'
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
("index", "python-hl7.tex", u"python-hl7 Documentation", u"John Paulett", "manual"),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
# latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
# latex_use_parts = False
# If true, show page references after internal links.
# latex_show_pagerefs = False
# If true, show URL addresses after external links.
# latex_show_urls = False
# Additional stuff for the LaTeX preamble.
# latex_preamble = ''
# Documents to append as an appendix to all manuals.
# latex_appendices = []
# If false, no module index is generated.
# latex_domain_indices = True
# -- Options for manual page output --------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [("mllp_send", "mllp_send", "MLLP network client", [u"John Paulett"], 1)]
# -- Options for Epub output ---------------------------------------------------
# Bibliographic Dublin Core info.
epub_title = u"python-hl7"
epub_author = u"John Paulett"
epub_publisher = u"John Paulett"
epub_copyright = u"2011, John Paulett"
# The language of the text. It defaults to the language option
# or en if the language is not set.
# epub_language = ''
# The scheme of the identifier. Typical schemes are ISBN or URL.
# epub_scheme = ''
# The unique identifier of the text. This can be a ISBN number
# or the project homepage.
# epub_identifier = ''
# A unique identification for the text.
# epub_uid = ''
# HTML files that should be inserted before the pages created by sphinx.
# The format is a list of tuples containing the path and title.
# epub_pre_files = []
# HTML files shat should be inserted after the pages created by sphinx.
# The format is a list of tuples containing the path and title.
# epub_post_files = []
# A list of files that should not be packed into the epub file.
# epub_exclude_files = []
# The depth of the table of contents in toc.ncx.
# epub_tocdepth = 3
# Allow duplicate toc entries.
# epub_tocdup = True
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {"http://docs.python.org/": None}
python-hl7-0.4.2/docs/contribute.rst 0000664 0000000 0000000 00000002525 14012562772 0017400 0 ustar 00root root 0000000 0000000 Contributing
============
The source code is available at http://github.com/johnpaulett/python-hl7
Please fork and issue pull requests. Generally any changes, bug fixes, or
new features should be accompanied by corresponding tests in our test
suite.
Testing
--------
The test suite is located in :file:`tests/` and can be run several ways.
It is recommended to run the full `tox `_ suite so
that all supported Python versions are tested and the documentation is built
and tested. We provide a :file:`Makefile` to create a virtualenv, install tox,
and run tox::
$ make tests
py27: commands succeeded
py26: commands succeeded
docs: commands succeeded
congratulations :)
To run the test suite with a specific python interpreter::
python setup.py test
To documentation is built by tox, but you can manually build via::
$ make docs
...
Doctest summary
===============
23 tests
0 failures in tests
0 failures in setup code
...
Formatting
----------
python-hl7 has converted to use `black `
to enforce a coding style. To automatically format using black and isort::
$ make format
It is also recommended to run the flake8 checks for PEP8 and PyFlake
violations. Commits should be free of warnings::
$ make lint
python-hl7-0.4.2/docs/index.rst 0000664 0000000 0000000 00000021421 14012562772 0016325 0 ustar 00root root 0000000 0000000 python-hl7 - Easy HL7 v2.x Parsing
==================================
python-hl7 is a simple library for parsing messages of Health Level 7
(HL7) version 2.x into Python objects. python-hl7 includes a simple
client that can send HL7 messages to a Minimal Lower Level Protocol (MLLP)
server (:ref:`mllp_send `).
HL7 is a communication protocol and message format for
health care data. It is the de-facto standard for transmitting data
between clinical information systems and between clinical devices.
The version 2.x series, which is often is a pipe delimited format
is currently the most widely accepted version of HL7 (there
is an alternative XML-based format).
python-hl7 currently only parses HL7 version 2.x messages into
an easy to access data structure. The library could eventually
also contain the ability to create HL7 v2.x messages.
python-hl7 parses HL7 into a series of wrapped :py:class:`hl7.Container` objects.
The there are specific subclasses of :py:class:`hl7.Container` depending on
the part of the HL7 message. The :py:class:`hl7.Container` message itself
is a subclass of a Python list, thus we can easily access the
HL7 message as an n-dimensional list. Specifically, the subclasses of
:py:class:`hl7.Container`, in order, are :py:class:`hl7.Message`,
:py:class:`hl7.Segment`, :py:class:`hl7.Field`, :py:class:`hl7.Repetition`.
and :py:class:`hl7.Component`.
python-hl7 includes experimental asyncio-based HL7 MLLP support in :doc:`mllp`,
which aims to replace `txHL7 `_.
.. image::
https://github.com/johnpaulett/python-hl7/workflows/Python%20package/badge.svg
:target: https://github.com/johnpaulett/python-hl7/actions
Result Tree
-----------
HL7 Messages have a limited number of levels. The top level is a Message.
A Message is comprised of a number of Fields (:py:class:`hl7.Field`).
Fields can repeat (:py:class:`hl7.Repetition`). The content of a field
is either a primitive data type (such as a string) or a composite
data type comprised of one or more Components (:py:class:`hl7.Component`). Components
are in turn comprised of Sub-Components (primitive data types).
The result of parsing is accessed as a tree using python list conventions:
``Message[segment][field][repetition][component][sub-component]``
The result can also be accessed using HL7 1-based indexing conventions by treating
each element as a callable:
``Message(segment)(field)(repetition)(component)(sub-component)``
Usage
-----
As an example, let's create a HL7 message:
.. doctest::
>>> message = 'MSH|^~\&|GHH LAB|ELAB-3|GHH OE|BLDG4|200202150930||ORU^R01|CNTRL-3456|P|2.4\r'
>>> message += 'PID|||555-44-4444||EVERYWOMAN^EVE^E^^^^L|JONES|196203520|F|||153 FERNWOOD DR.^^STATESVILLE^OH^35292||(206)3345232|(206)752-121||||AC555444444||67-A4335^OH^20030520\r'
>>> message += 'OBR|1|845439^GHH OE|1045813^GHH LAB|1554-5^GLUCOSE|||200202150730||||||||555-55-5555^PRIMARY^PATRICIA P^^^^MD^^LEVEL SEVEN HEALTHCARE, INC.|||||||||F||||||444-44-4444^HIPPOCRATES^HOWARD H^^^^MD\r'
>>> message += 'OBX|1|SN|1554-5^GLUCOSE^POST 12H CFST:MCNC:PT:SER/PLAS:QN||^182|mg/dl|70_105|H|||F\r'
We call the :py:func:`hl7.parse` command with string message:
.. doctest::
>>> import hl7
>>> h = hl7.parse(message)
We get a :py:class:`hl7.Message` object, wrapping a series of
:py:class:`hl7.Segment` objects:
.. doctest::
>>> type(h)
We can always get the HL7 message back:
.. doctest::
>>> str(h) == message
True
Interestingly, :py:class:`hl7.Message` can be accessed as a list:
.. doctest::
>>> isinstance(h, list)
True
There were 4 segments (MSH, PID, OBR, OBX):
.. doctest::
>>> len(h)
4
We can extract the :py:class:`hl7.Segment` from the
:py:class:`hl7.Message` instance:
.. doctest::
>>> h[3]
[['OBX'], ['1'], ['SN'], [[['1554-5'], ['GLUCOSE'], ['POST 12H CFST:MCNC:PT:SER/PLAS:QN']]], [''], [[[''], ['182']]], ['mg/dl'], ['70_105'], ['H'], [''], [''], ['F']]
>>> h[3] is h(4)
True
Note that since the first element of the segment is the segment name,
segments are effectively 1-based in python as well (because the HL7 spec does
not count the segment name as part of the segment itself):
.. doctest::
>>> h[3][0]
['OBX']
>>> h[3][1]
['1']
>>> h[3][2]
['SN']
>>> h(4)(2)
['SN']
We can easily reconstitute this segment as HL7, using the
appropriate separators:
.. doctest::
>>> str(h[3])
'OBX|1|SN|1554-5^GLUCOSE^POST 12H CFST:MCNC:PT:SER/PLAS:QN||^182|mg/dl|70_105|H|||F'
We can extract individual elements of the message:
.. doctest::
>>> h[3][3][0][1][0]
'GLUCOSE'
>>> h[3][3][0][1][0] is h(4)(3)(1)(2)(1)
True
>>> h[3][5][0][1][0]
'182'
>>> h[3][5][0][1][0] is h(4)(5)(1)(2)(1)
True
We can look up segments by the segment identifier, either via
:py:meth:`hl7.Message.segments` or via the traditional dictionary
syntax:
.. doctest::
>>> h.segments('OBX')[0][3][0][1][0]
'GLUCOSE'
>>> h['OBX'][0][3][0][1][0]
'GLUCOSE'
>>> h['OBX'][0][3][0][1][0] is h['OBX'](1)(3)(1)(2)(1)
True
Since many many types of segments only have a single instance in a message
(e.g. PID or MSH), :py:meth:`hl7.Message.segment` provides a convienance
wrapper around :py:meth:`hl7.Message.segments` that returns the first matching
:py:class:`hl7.Segment`:
.. doctest::
>>> h.segment('PID')[3][0]
'555-44-4444'
>>> h.segment('PID')[3][0] is h.segment('PID')(3)(1)
True
The result of parsing contains up to 5 levels. The last level is a non-container
type.
.. doctest::
>>> type(h)
>>> type(h[3])
>>> type(h[3][3])
>>> type(h[3][3][0])
>>> type(h[3][3][0][1])
>>> type(h[3][3][0][1][0])
The parser only generates the levels which are present in the message.
.. doctest::
>>> type(h[3][1])
>>> type(h[3][1][0])
MLLP network client - ``mllp_send``
-----------------------------------
python-hl7 features a simple network client, ``mllp_send``, which reads HL7
messages from a file or ``sys.stdin`` and posts them to an MLLP server.
``mllp_send`` is a command-line wrapper around
:py:class:`hl7.client.MLLPClient`. ``mllp_send`` is a useful tool for
testing HL7 interfaces or resending logged messages::
mllp_send --file sample.hl7 --port 6661 mirth.example.com
See :doc:`mllp_send` for examples and usage instructions.
For receiving HL7 messages using the Minimal Lower Level Protocol (MLLP), take a
look at the related `twisted-hl7 `_ package.
If do not want to use twisted and are looking to re-write some of twisted-hl7's
functionality, please reach out to us. It is likely that some of the MLLP
parsing and formatting can be moved into python-hl7, which twisted-hl7 and other
libraries can depend upon.
.. _unicode-vs-byte-strings:
Python 2 vs Python 3 and Unicode vs Byte strings
-------------------------------------------------
python-hl7 supports Python 3.5+ and primarily deals with the unicode ``str`` type.
Passing bytes to :py:func:`hl7.parse`, requires setting the
``encoding`` parameter, if using anything other than UTF-8. :py:func:`hl7.parse`
will always return a datastructure containing unicode ``str`` objects.
:py:class:`hl7.Message` can be forced back into a single string using
and ``str(message)``.
:doc:`mllp_send` assumes the stream is already in the correct encoding.
:py:class:`hl7.client.MLLPClient`, if given a ``str`` or
:py:class:`hl7.Message` instance, will use its ``encoding`` method
to encode the unicode data into bytes.
Contents
--------
.. toctree::
:maxdepth: 1
api
mllp_send
mllp
accessors
contribute
changelog
authors
license
Install
-------
python-hl7 is available on `PyPi `_
via ``pip`` or ``easy_install``::
pip install -U hl7
For recent versions of Debian and Ubuntu, the *python-hl7* package is
available::
sudo apt-get install python-hl7
Links
-----
* Documentation: http://python-hl7.readthedocs.org
* Source Code: http://github.com/johnpaulett/python-hl7
* PyPi: http://pypi.python.org/pypi/hl7
HL7 References:
* `Health Level 7 - Wikipedia `_
* `nule.org's Introduction to HL7 `_
* `hl7.org `_
* `OpenMRS's HL7 documentation `_
* `Transport Specification: MLLP `_
* `HL7v2 Parsing `_
* `HL7 Book `_
python-hl7-0.4.2/docs/license.rst 0000664 0000000 0000000 00000000070 14012562772 0016635 0 ustar 00root root 0000000 0000000 License
=======
.. include:: ../LICENSE
:literal:
python-hl7-0.4.2/docs/make.bat 0000664 0000000 0000000 00000010647 14012562772 0016101 0 ustar 00root root 0000000 0000000 @ECHO OFF
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set BUILDDIR=_build
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
if NOT "%PAPER%" == "" (
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
)
if "%1" == "" goto help
if "%1" == "help" (
:help
echo.Please use `make ^` where ^ is one of
echo. html to make standalone HTML files
echo. dirhtml to make HTML files named index.html in directories
echo. singlehtml to make a single large HTML file
echo. pickle to make pickle files
echo. json to make JSON files
echo. htmlhelp to make HTML files and a HTML help project
echo. qthelp to make HTML files and a qthelp project
echo. devhelp to make HTML files and a Devhelp project
echo. epub to make an epub
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
echo. text to make text files
echo. man to make manual pages
echo. changes to make an overview over all changed/added/deprecated items
echo. linkcheck to check all external links for integrity
echo. doctest to run all doctests embedded in the documentation if enabled
goto end
)
if "%1" == "clean" (
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
del /q /s %BUILDDIR%\*
goto end
)
if "%1" == "html" (
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
goto end
)
if "%1" == "dirhtml" (
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
goto end
)
if "%1" == "singlehtml" (
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
goto end
)
if "%1" == "pickle" (
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the pickle files.
goto end
)
if "%1" == "json" (
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the JSON files.
goto end
)
if "%1" == "htmlhelp" (
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run HTML Help Workshop with the ^
.hhp project file in %BUILDDIR%/htmlhelp.
goto end
)
if "%1" == "qthelp" (
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run "qcollectiongenerator" with the ^
.qhcp project file in %BUILDDIR%/qthelp, like this:
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\python-hl7.qhcp
echo.To view the help file:
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\python-hl7.ghc
goto end
)
if "%1" == "devhelp" (
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished.
goto end
)
if "%1" == "epub" (
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The epub file is in %BUILDDIR%/epub.
goto end
)
if "%1" == "latex" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
if errorlevel 1 exit /b 1
echo.
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "text" (
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The text files are in %BUILDDIR%/text.
goto end
)
if "%1" == "man" (
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The manual pages are in %BUILDDIR%/man.
goto end
)
if "%1" == "changes" (
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
if errorlevel 1 exit /b 1
echo.
echo.The overview file is in %BUILDDIR%/changes.
goto end
)
if "%1" == "linkcheck" (
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
if errorlevel 1 exit /b 1
echo.
echo.Link check complete; look for any errors in the above output ^
or in %BUILDDIR%/linkcheck/output.txt.
goto end
)
if "%1" == "doctest" (
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
if errorlevel 1 exit /b 1
echo.
echo.Testing of doctests in the sources finished, look at the ^
results in %BUILDDIR%/doctest/output.txt.
goto end
)
:end
python-hl7-0.4.2/docs/mllp.rst 0000664 0000000 0000000 00000010201 14012562772 0016154 0 ustar 00root root 0000000 0000000 MLLP using asyncio
==================
.. versionadded:: 0.4.1
.. note::
`hl7.mllp` package is currently experimental and subject to change.
It aims to replace txHL7.
python-hl7 includes classes for building HL7 clients and
servers using asyncio. The underlying protocol for these
clients and servers is MLLP.
The `hl7.mllp` package is designed the same as
the `asyncio.streams` package. `Examples in that documentation
`_
may be of assistance in writing production senders and
receivers.
HL7 Sender
----------
.. code:: python
# Using the third party `aiorun` instead of the `asyncio.run()` to avoid
# boilerplate.
import aiorun
import hl7
from hl7.mllp import open_hl7_connection
async def main():
message = 'MSH|^~\&|GHH LAB|ELAB-3|GHH OE|BLDG4|200202150930||ORU^R01|CNTRL-3456|P|2.4\r'
message += 'PID|||555-44-4444||EVERYWOMAN^EVE^E^^^^L|JONES|196203520|F|||153 FERNWOOD DR.^^STATESVILLE^OH^35292||(206)3345232|(206)752-121||||AC555444444||67-A4335^OH^20030520\r'
message += 'OBR|1|845439^GHH OE|1045813^GHH LAB|1554-5^GLUCOSE|||200202150730||||||||555-55-5555^PRIMARY^PATRICIA P^^^^MD^^LEVEL SEVEN HEALTHCARE, INC.|||||||||F||||||444-44-4444^HIPPOCRATES^HOWARD H^^^^MD\r'
message += 'OBX|1|SN|1554-5^GLUCOSE^POST 12H CFST:MCNC:PT:SER/PLAS:QN||^182|mg/dl|70_105|H|||F\r'
# Open the connection to the HL7 receiver.
# Using wait_for is optional, but recommended so
# a dead receiver won't block you for long
hl7_reader, hl7_writer = await asyncio.wait_for(
open_hl7_connection("127.0.0.1", 2575),
timeout=10,
)
hl7_message = hl7.parse(message)
# Write the HL7 message, and then wait for the writer
# to drain to actually send the message
hl7_writer.writemessage(hl7_message)
await hl7_writer.drain()
print(f'Sent message\n {hl7_message}'.replace('\r', '\n'))
# Now wait for the ACK message from the receiever
hl7_ack = await asyncio.wait_for(
hl7_reader.readmessage(),
timeout=10
)
print(f'Received ACK\n {hl7_ack}'.replace('\r', '\n'))
aiorun.run(main(), stop_on_unhandled_errors=True)
HL7 Receiver
------------
.. code:: python
# Using the third party `aiorun` instead of the `asyncio.run()` to avoid
# boilerplate.
import aiorun
import hl7
from hl7.mllp import start_hl7_server
async def process_hl7_messages(hl7_reader, hl7_writer):
"""This will be called every time a socket connects
with us.
"""
peername = hl7_writer.get_extra_info("peername")
print(f"Connection established {peername}")
try:
# We're going to keep listening until the writer
# is closed. Only writers have closed status.
while not hl7_writer.is_closing():
hl7_message = await hl7_reader.readmessage()
print(f'Received message\n {hl7_message}'.replace('\r', '\n'))
# Now let's send the ACK and wait for the
# writer to drain
hl7_writer.writemessage(hl7_message.create_ack())
await hl7_writer.drain()
except asyncio.IncompleteReadError:
# Oops, something went wrong, if the writer is not
# closed or closing, close it.
if not hl7_writer.is_closing():
hl7_writer.close()
await hl7_writer.wait_closed()
print(f"Connection closed {peername}")
async def main():
try:
# Start the server in a with clause to make sure we
# close it
async with await start_hl7_server(
process_hl7_messages, port=2575
) as hl7_server:
# And now we server forever. Or until we are
# cancelled...
await hl7_server.serve_forever()
except asyncio.CancelledError:
# Cancelled errors are expected
pass
except Exception:
print("Error occurred in main")
aiorun.run(main(), stop_on_unhandled_errors=True)
python-hl7-0.4.2/docs/mllp_send.rst 0000664 0000000 0000000 00000003626 14012562772 0017202 0 ustar 00root root 0000000 0000000 .. _mllp-send:
===================================
``mllp_send`` - MLLP network client
===================================
python-hl7 features a simple network client, ``mllp_send``, which reads HL7
messages from a file or ``sys.stdin`` and posts them to an MLLP server.
``mllp_send`` is a command-line wrapper around
:py:class:`hl7.client.MLLPClient`. ``mllp_send`` is a useful tool for
testing HL7 interfaces or resending logged messages::
$ mllp_send --file sample.hl7 --port 6661 mirth.example.com
MSH|^~\&|LIS|Example|Hospital|Mirth|20111207105244||ACK^A01|A234244|P|2.3.1|
MSA|AA|234242|Message Received Successfully|
Usage
=====
::
Usage: mllp_send [options]
Options:
-h, --help show this help message and exit
--version print current version and exit
-p PORT, --port=PORT port to connect to
-f FILE, --file=FILE read from FILE instead of stdin
-q, --quiet do not print status messages to stdout
--loose allow file to be a HL7-like object (\r\n instead of
\r). Requires that messages start with "MSH|^~\&|".
Requires --file option (no stdin)
Input Format
============
By default, ``mllp_send`` expects the ``FILE`` or stdin input to be a properly
formatted HL7 message (carriage returns separating segments) wrapped in a MLLP
stream (``message1message2...``).
However, it is common, especially if the file has been manually edited in
certain text editors, that the ASCII control characters will be lost and the
carriage returns will be replaced with the platform's default line endings.
In this case, ``mllp_send`` provides the ``--loose`` option, which attempts
to take something that "looks like HL7" and convert it into a proper HL7
message..
Additional Resources
====================
* http://python-hl7.readthedocs.org
python-hl7-0.4.2/hl7/ 0000775 0000000 0000000 00000000000 14012562772 0014226 5 ustar 00root root 0000000 0000000 python-hl7-0.4.2/hl7/__init__.py 0000664 0000000 0000000 00000003056 14012562772 0016343 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
"""python-hl7 is a simple library for parsing messages of Health Level 7
(HL7) version 2.x into Python objects.
* Documentation: http://python-hl7.readthedocs.org
* Source Code: http://github.com/johnpaulett/python-hl7
"""
from .accessor import Accessor
from .containers import (
Batch,
Component,
Container,
Factory,
Field,
File,
Message,
Repetition,
Segment,
Sequence,
)
from .datatypes import parse_datetime
from .exceptions import (
HL7Exception,
MalformedBatchException,
MalformedFileException,
MalformedSegmentException,
ParseException,
)
from .parser import parse, parse_batch, parse_file, parse_hl7
from .util import generate_message_control_id, isbatch, isfile, ishl7, split_file
from .version import get_version
__version__ = get_version()
__author__ = "John Paulett"
__email__ = "john -at- paulett.org"
__license__ = "BSD"
__copyright__ = "Copyright 2011, John Paulett "
#: This is the HL7 Null value. It means that a field is present and blank.
NULL = '""'
__all__ = [
"parse",
"parse_hl7",
"parse_batch",
"parse_file",
"Sequence",
"Container",
"File",
"Batch",
"Message",
"Segment",
"Field",
"Repetition",
"Component",
"Factory",
"Accessor",
"ishl7",
"isbatch",
"isfile",
"split_file",
"generate_message_control_id",
"parse_datetime",
"HL7Exception",
"MalformedBatchException",
"MalformedFileException",
"MalformedSegmentException",
"ParseException",
]
python-hl7-0.4.2/hl7/accessor.py 0000664 0000000 0000000 00000005606 14012562772 0016411 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
from collections import namedtuple
class Accessor(
namedtuple(
"Accessor",
[
"segment",
"segment_num",
"field_num",
"repeat_num",
"component_num",
"subcomponent_num",
],
)
):
__slots__ = ()
def __new__(
cls,
segment,
segment_num=1,
field_num=None,
repeat_num=None,
component_num=None,
subcomponent_num=None,
):
"""Create a new instance of Accessor for *segment*. Index numbers start from 1."""
return super(Accessor, cls).__new__(
cls,
segment,
segment_num,
field_num,
repeat_num,
component_num,
subcomponent_num,
)
@property
def key(self):
"""Return the string accessor key that represents this instance"""
seg = (
self.segment
if self.segment_num == 1
else self.segment + str(self.segment_num)
)
return ".".join(
str(f)
for f in [
seg,
self.field_num,
self.repeat_num,
self.component_num,
self.subcomponent_num,
]
if f is not None
)
def __str__(self):
return self.key
@classmethod
def parse_key(cls, key):
"""Create an Accessor by parsing an accessor key.
The key is defined as:
| SEG[n]-Fn-Rn-Cn-Sn
| F Field
| R Repeat
| C Component
| S Sub-Component
|
| *Indexing is from 1 for compatibility with HL7 spec numbering.*
Example:
| PID.F1.R1.C2.S2 or PID.1.1.2.2
|
| PID (default to first PID segment, counting from 1)
| F1 (first after segment id, HL7 Spec numbering)
| R1 (repeat counting from 1)
| C2 (component 2 counting from 1)
| S2 (component 2 counting from 1)
"""
def parse_part(keyparts, index, prefix):
if len(keyparts) > index:
num = keyparts[index]
if num[0].upper() == prefix:
num = num[1:]
return int(num)
else:
return None
parts = key.split(".")
segment = parts[0][:3]
if len(parts[0]) > 3:
segment_num = int(parts[0][3:])
else:
segment_num = 1
field_num = parse_part(parts, 1, "F")
repeat_num = parse_part(parts, 2, "R")
component_num = parse_part(parts, 3, "C")
subcomponent_num = parse_part(parts, 4, "S")
return cls(
segment, segment_num, field_num, repeat_num, component_num, subcomponent_num
)
python-hl7-0.4.2/hl7/client.py 0000664 0000000 0000000 00000016560 14012562772 0016066 0 ustar 00root root 0000000 0000000 import io
import os.path
import socket
import sys
from optparse import OptionParser
import hl7
SB = b"\x0b" # , vertical tab
EB = b"\x1c" # , file separator
CR = b"\x0d" # , \r
FF = b"\x0c" # , new page form feed
RECV_BUFFER = 4096
class MLLPException(Exception):
pass
class MLLPClient(object):
"""
A basic, blocking, HL7 MLLP client based upon :py:mod:`socket`.
MLLPClient implements two methods for sending data to the server.
* :py:meth:`MLLPClient.send` for raw data that already is wrapped in the
appropriate MLLP container (e.g. *message*).
* :py:meth:`MLLPClient.send_message` will wrap the message in the MLLP
container
Can be used by the ``with`` statement to ensure :py:meth:`MLLPClient.close`
is called::
with MLLPClient(host, port) as client:
client.send_message('MSH|...')
MLLPClient takes an optional ``encoding`` parameter, defaults to UTF-8,
for encoding unicode messages [#]_.
.. [#] http://wiki.hl7.org/index.php?title=Character_Set_used_in_v2_messages
"""
def __init__(self, host, port, encoding="utf-8"):
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.connect((host, port))
self.encoding = encoding
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, trackeback):
self.close()
def close(self):
"""Release the socket connection"""
self.socket.close()
def send_message(self, message):
"""Wraps a byte string, unicode string, or :py:class:`hl7.Message`
in a MLLP container and send the message to the server
If message is a byte string, we assume it is already encoded properly.
If message is unicode or :py:class:`hl7.Message`, it will be encoded
according to :py:attr:`hl7.client.MLLPClient.encoding`
"""
if isinstance(message, bytes):
# Assume we have the correct encoding
binary = message
else:
# Encode the unicode message into a bytestring
if isinstance(message, hl7.Message):
message = str(message)
binary = message.encode(self.encoding)
# wrap in MLLP message container
data = SB + binary + EB + CR
return self.send(data)
def send(self, data):
"""Low-level, direct access to the socket.send (data must be already
wrapped in an MLLP container). Blocks until the server returns.
"""
# upload the data
self.socket.send(data)
# wait for the ACK/NACK
return self.socket.recv(RECV_BUFFER)
# wrappers to make testing easier
def stdout(content):
# In Python 3, can't write bytes via sys.stdout.write
# http://bugs.python.org/issue18512
if isinstance(content, bytes):
out = sys.stdout.buffer
newline = b"\n"
else:
out = sys.stdout
newline = "\n"
out.write(content + newline)
def stdin():
return sys.stdin
def stderr():
return sys.stderr
def read_stream(stream):
"""Buffer the stream and yield individual, stripped messages"""
_buffer = b""
while True:
data = stream.read(RECV_BUFFER)
if data == b"":
break
# usually should be broken up by EB, but I have seen FF separating
# messages
messages = (_buffer + data).split(EB if FF not in data else FF)
# whatever is in the last chunk is an uncompleted message, so put back
# into the buffer
_buffer = messages.pop(-1)
for m in messages:
yield m.strip(SB + CR)
if len(_buffer.strip()) > 0:
raise MLLPException("buffer not terminated: %s" % _buffer)
def read_loose(stream):
"""Turn a HL7-like blob of text into a real HL7 messages"""
# look for the START_BLOCK to delineate messages
START_BLOCK = rb"MSH|^~\&|"
# load all the data
data = stream.read()
# Take out all the typical MLLP separators. In Python 3, iterating
# through a bytestring returns ints, so we need to filter out the int
# versions of the separators, then convert back from a list of ints to
# a bytestring.
# WARNING: There is an assumption here that we can treat the data as single bytes
# when filtering out the separators.
separators = [bs[0] for bs in [EB, FF, SB]]
data = bytes(b for b in data if b not in separators)
# Windows & Unix new lines to segment separators
data = data.replace(b"\r\n", b"\r").replace(b"\n", b"\r")
for m in data.split(START_BLOCK):
if not m:
# the first element will not have any data from the split
continue
# strip any trailing whitespace
m = m.strip(CR + b"\n ")
# re-insert the START_BLOCK, which was removed via the split
yield START_BLOCK + m
def mllp_send():
"""Command line tool to send messages to an MLLP server"""
# set up the command line options
script_name = os.path.basename(sys.argv[0])
parser = OptionParser(usage=script_name + " [options] ")
parser.add_option(
"--version",
action="store_true",
dest="version",
default=False,
help="print current version and exit",
)
parser.add_option(
"-p",
"--port",
action="store",
type="int",
dest="port",
default=6661,
help="port to connect to",
)
parser.add_option(
"-f",
"--file",
dest="filename",
help="read from FILE instead of stdin",
metavar="FILE",
)
parser.add_option(
"-q",
"--quiet",
action="store_true",
dest="verbose",
default=True,
help="do not print status messages to stdout",
)
parser.add_option(
"--loose",
action="store_true",
dest="loose",
default=False,
help=(
"allow file to be a HL7-like object (\\r\\n instead "
"of \\r). Requires that messages start with "
'"MSH|^~\\&|". Requires --file option (no stdin)'
),
)
(options, args) = parser.parse_args()
if options.version:
import hl7
stdout(hl7.__version__)
return
if len(args) == 1:
host = args[0]
else:
# server not present
parser.print_usage()
stderr().write("server required\n")
sys.exit(1)
return # for testing when sys.exit mocked
if options.filename is not None:
# Previously set stream to the open() handle, but then we did not
# close the open file handle. This new approach consumes the entire
# file into memory before starting to process, which is not required
# or ideal, since we can handle a stream
with open(options.filename, "rb") as f:
stream = io.BytesIO(f.read())
else:
if options.loose:
stderr().write("--loose requires --file\n")
sys.exit(1)
return # for testing when sys.exit mocked
stream = stdin()
with MLLPClient(host, options.port) as client:
message_stream = (
read_stream(stream) if not options.loose else read_loose(stream)
)
for message in message_stream:
result = client.send_message(message)
if options.verbose:
stdout(result)
if __name__ == "__main__":
mllp_send()
python-hl7-0.4.2/hl7/containers.py 0000664 0000000 0000000 00000074365 14012562772 0016764 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
import datetime
import logging
from .accessor import Accessor
from .exceptions import (
MalformedBatchException,
MalformedFileException,
MalformedSegmentException,
)
from .util import generate_message_control_id
logger = logging.getLogger(__file__)
_SENTINEL = object()
class Sequence(list):
"""Base class for sequences that can be indexed using 1-based index"""
def __call__(self, index, value=_SENTINEL):
"""Support list access using HL7 compatible 1-based indices.
Can be used to get and set values.
>>> s = hl7.Sequence([1, 2, 3, 4])
>>> s(1) == s[0]
True
>>> s(2, "new")
>>> s
[1, 'new', 3, 4]
"""
index = self._adjust_index(int(index))
if value is _SENTINEL:
return self[index]
else:
self[index] = value
def _adjust_index(self, index):
"""Subclasses can override if they do not want HL7 1-based indexing when used as callable"""
if index >= 1:
return index - 1
else:
return index
class Container(Sequence):
"""Abstract root class for the parts of the HL7 message."""
def __init__(
self, separator, sequence=[], esc="\\", separators="\r|~^&", factory=None
):
# Initialize the list object, optionally passing in the
# sequence. Since list([]) == [], using the default
# parameter will not cause any issues.
super(Container, self).__init__(sequence)
self.separator = separator
self.esc = esc
self.separators = separators
self.factory = factory if factory is not None else Factory
def __getitem__(self, item):
# Python slice operator was returning a regular list, not a
# Container subclass
sequence = super(Container, self).__getitem__(item)
if isinstance(item, slice):
return self.__class__(
self.separator,
sequence,
self.esc,
self.separators,
factory=self.factory,
)
return sequence
def __getslice__(self, i, j):
# Python 2.x compatibility. __getslice__ is deprecated, and
# we want to wrap the logic from __getitem__ when handling slices
return self.__getitem__(slice(i, j))
def __str__(self):
return self.separator.join((str(x) for x in self))
class BuilderMixin(object):
"""Mixin class that allows for the create functions
in the top-level container classes
"""
def create_file(self, seq):
"""Create a new :py:class:`hl7.File` compatible with this container"""
return self.factory.create_file(
self.separators[0],
seq,
esc=self.esc,
separators=self.separators,
factory=self.factory,
)
def create_batch(self, seq):
"""Create a new :py:class:`hl7.Batch` compatible with this container"""
return self.factory.create_batch(
self.separators[0],
seq,
esc=self.esc,
separators=self.separators,
factory=self.factory,
)
def create_message(self, seq):
"""Create a new :py:class:`hl7.Message` compatible with this container"""
return self.factory.create_message(
self.separators[0],
seq,
esc=self.esc,
separators=self.separators,
factory=self.factory,
)
def create_segment(self, seq):
"""Create a new :py:class:`hl7.Segment` compatible with this container"""
return self.factory.create_segment(
self.separators[1],
seq,
esc=self.esc,
separators=self.separators[1:],
factory=self.factory,
)
def create_field(self, seq):
"""Create a new :py:class:`hl7.Field` compatible with this container"""
return self.factory.create_field(
self.separators[2],
seq,
esc=self.esc,
separators=self.separators[2:],
factory=self.factory,
)
def create_repetition(self, seq):
"""Create a new :py:class:`hl7.Repetition` compatible with this container"""
return self.factory.create_repetition(
self.separators[3],
seq,
esc=self.esc,
separators=self.separators[3:],
factory=self.factory,
)
def create_component(self, seq):
"""Create a new :py:class:`hl7.Component` compatible with this container"""
return self.factory.create_component(
self.separators[4],
seq,
esc=self.esc,
separators=self.separators[4:],
factory=self.factory,
)
class File(Container, BuilderMixin):
"""Representation of an HL7 file from the batch protocol.
It contains a list of :py:class:`hl7.Batch`
instances. It may contain FHS/FTS :py:class:`hl7.Segment` instances.
Files may or may not be wrapped in FHS/FTS segements
deliniating the start/end of the batch. These are optional.
"""
def __init__(
self, separator, sequence=[], esc="\\", separators="\r|~^&", factory=None
):
super(File, self).__init__(
separator,
sequence=sequence,
esc=esc,
separators=separators,
factory=factory,
)
self.header = None
self.trailer = None
@property
def header(self):
"""FHS :py:class:`hl7.Segment`"""
return self._batch_header_segment
@header.setter
def header(self, segment):
if segment and segment[0][0] != "FHS":
raise MalformedSegmentException('header must begin with "FHS"')
self._batch_header_segment = segment
@property
def trailer(self):
"""FTS :py:class:`hl7.Segment`"""
return self._batch_trailer_segment
@trailer.setter
def trailer(self, segment):
if segment and segment[0][0] != "FTS":
raise MalformedSegmentException('trailer must begin with "FTS"')
self._batch_trailer_segment = segment
def create_header(self):
"""Create a new :py:class:`hl7.Segment` FHS compatible with this file"""
return self.create_segment(
[
self.create_field(["FHS"]),
self.create_field([self.separators[1]]),
self.create_field(
[
self.separators[3]
+ self.separators[2]
+ self.esc
+ self.separators[4]
]
),
]
)
def create_trailer(self):
"""Create a new :py:class:`hl7.Segment` FTS compatible with this file"""
return self.create_segment([self.create_field(["FTS"])])
def __str__(self):
"""Join a the child batches into a single string, separated
by the self.separator. This method acts recursively, calling
the children's __unicode__ method. Thus ``unicode()`` is the
approriate method for turning the python-hl7 representation of
HL7 into a standard string.
If this batch has FHS/FTS segments, they will be added to the
beginning/end of the returned string.
"""
if (self.header and not self.trailer) or (not self.header and self.trailer):
raise MalformedFileException(
"Either both header and trailer must be present or neither"
)
return (
super(File, self).__str__()
if not self.header
else str(self.header)
+ self.separator
+ super(File, self).__str__()
+ str(self.trailer)
+ self.separator
)
class Batch(Container, BuilderMixin):
"""Representation of an HL7 batch from the batch protocol.
It contains a list of :py:class:`hl7.Message` instances.
It may contain BHS/BTS :py:class:`hl7.Segment` instances.
Batches may or may not be wrapped in BHS/BTS segements
deliniating the start/end of the batch. These are optional.
"""
def __init__(
self, separator, sequence=[], esc="\\", separators="\r|~^&", factory=None
):
super(Batch, self).__init__(
separator,
sequence=sequence,
esc=esc,
separators=separators,
factory=factory,
)
self.header = None
self.trailer = None
@property
def header(self):
"""BHS :py:class:`hl7.Segment`"""
return self._batch_header_segment
@header.setter
def header(self, segment):
if segment and segment[0][0] != "BHS":
raise MalformedSegmentException('header must begin with "BHS"')
self._batch_header_segment = segment
@property
def trailer(self):
"""BTS :py:class:`hl7.Segment`"""
return self._batch_trailer_segment
@trailer.setter
def trailer(self, segment):
if segment and segment[0][0] != "BTS":
raise MalformedSegmentException('trailer must begin with "BTS"')
self._batch_trailer_segment = segment
def create_header(self):
"""Create a new :py:class:`hl7.Segment` BHS compatible with this batch"""
return self.create_segment(
[
self.create_field(["BHS"]),
self.create_field([self.separators[1]]),
self.create_field(
[
self.separators[3]
+ self.separators[2]
+ self.esc
+ self.separators[4]
]
),
]
)
def create_trailer(self):
"""Create a new :py:class:`hl7.Segment` BHS compatible with this batch"""
return self.create_segment([self.create_field(["BTS"])])
def __str__(self):
"""Join a the child messages into a single string, separated
by the self.separator. This method acts recursively, calling
the children's __unicode__ method. Thus ``unicode()`` is the
approriate method for turning the python-hl7 representation of
HL7 into a standard string.
If this batch has BHS/BTS segments, they will be added to the
beginning/end of the returned string.
"""
if (self.header and not self.trailer) or (not self.header and self.trailer):
raise MalformedBatchException(
"Either both header and trailer must be present or neither"
)
return (
super(Batch, self).__str__()
if not self.header
else str(self.header)
+ self.separator
+ super(Batch, self).__str__()
+ str(self.trailer)
+ self.separator
)
class Message(Container, BuilderMixin):
"""Representation of an HL7 message. It contains a list
of :py:class:`hl7.Segment` instances.
"""
def __getitem__(self, key):
"""Index, segment-based or accessor lookup.
If key is an integer, ``__getitem__`` acts list a list, returning
the :py:class:`hl7.Segment` held at that index:
>>> h[1] # doctest: +ELLIPSIS
[['PID'], ...]
If the key is a string of length 3, ``__getitem__`` acts like a dictionary,
returning all segments whose *segment_id* is *key*
(alias of :py:meth:`hl7.Message.segments`).
>>> h['OBX'] # doctest: +ELLIPSIS
[[['OBX'], ['1'], ...]]
If the key is a string of length greater than 3,
the key is parsed into an :py:class:`hl7.Accessor` and passed
to :py:meth:`hl7.Message.extract_field`.
If the key is an :py:class:`hl7.Accessor`, it is passed to
:py:meth:`hl7.Message.extract_field`.
"""
if isinstance(key, str):
if len(key) == 3:
return self.segments(key)
return self.extract_field(*Accessor.parse_key(key))
elif isinstance(key, Accessor):
return self.extract_field(*key)
return super(Message, self).__getitem__(key)
def __setitem__(self, key, value):
"""Index or accessor assignment.
If key is an integer, ``__setitem__`` acts list a list, setting
the :py:class:`hl7.Segment` held at that index:
>>> h[1] = hl7.Segment("|", [hl7.Field("^", ['PID'], [''])])
If the key is a string of length greater than 3,
the key is parsed into an :py:class:`hl7.Accessor` and passed
to :py:meth:`hl7.Message.assign_field`.
>>> h["PID.2"] = "NEW"
If the key is an :py:class:`hl7.Accessor`, it is passed to
:py:meth:`hl7.Message.assign_field`.
"""
if isinstance(key, str) and len(key) > 3 and isinstance(value, str):
return self.assign_field(value, *Accessor.parse_key(key))
elif isinstance(key, Accessor):
return self.assign_field(value, *key)
return super(Message, self).__setitem__(key, value)
def segment(self, segment_id):
"""Gets the first segment with the *segment_id* from the parsed
*message*.
>>> h.segment('PID') # doctest: +ELLIPSIS
[['PID'], ...]
:rtype: :py:class:`hl7.Segment`
"""
# Get the list of all the segments and pull out the first one,
# if possible
match = self.segments(segment_id)
# We should never get an IndexError, since segments will instead
# throw an KeyError
return match[0]
def segments(self, segment_id):
"""Returns the requested segments from the parsed *message* that are
identified by the *segment_id* (e.g. OBR, MSH, ORC, OBX).
>>> h.segments('OBX')
[[['OBX'], ['1'], ...]]
:rtype: list of :py:class:`hl7.Segment`
"""
# Compare segment_id to the very first string in each segment,
# returning all segments that match.
# Return as a Sequence so 1-based indexing can be used
matches = Sequence(segment for segment in self if segment[0][0] == segment_id)
if len(matches) == 0:
raise KeyError("No %s segments" % segment_id)
return matches
def extract_field(
self,
segment,
segment_num=1,
field_num=1,
repeat_num=1,
component_num=1,
subcomponent_num=1,
):
"""
Extract a field using a future proofed approach, based on rules in:
http://wiki.medical-objects.com.au/index.php/Hl7v2_parsing
'PID|Field1|Component1^Component2|Component1^Sub-Component1&Sub-Component2^Component3|Repeat1~Repeat2',
| PID.F3.R1.C2.S2 = 'Sub-Component2'
| PID.F4.R2.C1 = 'Repeat1'
Compatibility Rules:
If the parse tree is deeper than the specified path continue
following the first child branch until a leaf of the tree is
encountered and return that value (which could be blank).
Example:
| PID.F3.R1.C2 = 'Sub-Component1' (assume .SC1)
If the parse tree terminates before the full path is satisfied
check each of the subsequent paths and if every one is specified
at position 1 then the leaf value reached can be returned as the
result.
| PID.F4.R1.C1.SC1 = 'Repeat1' (ignore .SC1)
"""
# Save original values for error messages
accessor = Accessor(
segment, segment_num, field_num, repeat_num, component_num, subcomponent_num
)
field_num = field_num or 1
repeat_num = repeat_num or 1
component_num = component_num or 1
subcomponent_num = subcomponent_num or 1
segment = self.segments(segment)(segment_num)
if field_num < len(segment):
field = segment(field_num)
else:
if repeat_num == 1 and component_num == 1 and subcomponent_num == 1:
return "" # Assume non-present optional value
raise IndexError("Field not present: {0}".format(accessor.key))
rep = field(repeat_num)
if not isinstance(rep, Repetition):
# leaf
if component_num == 1 and subcomponent_num == 1:
return (
rep
if accessor.segment == "MSH" and accessor.field_num in (1, 2)
else self.unescape(rep)
)
raise IndexError(
"Field reaches leaf node before completing path: {0}".format(
accessor.key
)
)
if component_num > len(rep):
if subcomponent_num == 1:
return "" # Assume non-present optional value
raise IndexError("Component not present: {0}".format(accessor.key))
component = rep(component_num)
if not isinstance(component, Component):
# leaf
if subcomponent_num == 1:
return self.unescape(component)
raise IndexError(
"Field reaches leaf node before completing path: {0}".format(
accessor.key
)
)
if subcomponent_num <= len(component):
subcomponent = component(subcomponent_num)
return self.unescape(subcomponent)
else:
return "" # Assume non-present optional value
def assign_field(
self,
value,
segment,
segment_num=1,
field_num=None,
repeat_num=None,
component_num=None,
subcomponent_num=None,
):
"""
Assign a value into a message using the tree based assignment notation.
The segment must exist.
Extract a field using a future proofed approach, based on rules in:
http://wiki.medical-objects.com.au/index.php/Hl7v2_parsing
"""
segment = self.segments(segment)(segment_num)
while len(segment) <= field_num:
segment.append(self.create_field([]))
field = segment(field_num)
if repeat_num is None:
field[:] = [value]
return
while len(field) < repeat_num:
field.append(self.create_repetition([]))
repetition = field(repeat_num)
if component_num is None:
repetition[:] = [value]
return
while len(repetition) < component_num:
repetition.append(self.create_component([]))
component = repetition(component_num)
if subcomponent_num is None:
component[:] = [value]
return
while len(component) < subcomponent_num:
component.append("")
component(subcomponent_num, value)
def escape(self, field, app_map=None):
"""
See: http://www.hl7standards.com/blog/2006/11/02/hl7-escape-sequences/
To process this correctly, the full set of separators (MSH.1/MSH.2) needs to be known.
Pass through the message. Replace recognised characters with their escaped
version. Return an ascii encoded string.
Functionality:
* Replace separator characters (2.10.4)
* replace application defined characters (2.10.7)
* Replace non-ascii values with hex versions using HL7 conventions.
Incomplete:
* replace highlight characters (2.10.3)
* How to handle the rich text substitutions.
* Merge contiguous hex values
"""
if not field:
return field
esc = str(self.esc)
DEFAULT_MAP = {
self.separators[1]: "F", # 2.10.4
self.separators[2]: "R",
self.separators[3]: "S",
self.separators[4]: "T",
self.esc: "E",
"\r": ".br", # 2.10.6
}
rv = []
for offset, c in enumerate(field):
if app_map and c in app_map:
rv.append(esc + app_map[c] + esc)
elif c in DEFAULT_MAP:
rv.append(esc + DEFAULT_MAP[c] + esc)
elif ord(c) >= 0x20 and ord(c) <= 0x7E:
rv.append(c)
else:
rv.append("%sX%2x%s" % (esc, ord(c), esc))
return "".join(rv)
def unescape(self, field, app_map=None): # noqa: C901
"""
See: http://www.hl7standards.com/blog/2006/11/02/hl7-escape-sequences/
To process this correctly, the full set of separators (MSH.1/MSH.2) needs to be known.
This will convert the identifiable sequences.
If the application provides mapping, these are also used.
Items which cannot be mapped are removed
For example, the App Map count provide N, H, Zxxx values
Chapter 2: Section 2.10
At the moment, this functionality can:
* replace the parsing characters (2.10.4)
* replace highlight characters (2.10.3)
* replace hex characters. (2.10.5)
* replace rich text characters (2.10.6)
* replace application defined characters (2.10.7)
It cannot:
* switch code pages / ISO IR character sets
"""
if not field or field.find(self.esc) == -1:
return field
DEFAULT_MAP = {
"H": "_", # Override using the APP MAP: 2.10.3
"N": "_", # Override using the APP MAP
"F": self.separators[1], # 2.10.4
"R": self.separators[2],
"S": self.separators[3],
"T": self.separators[4],
"E": self.esc,
".br": "\r", # 2.10.6
".sp": "\r",
".fi": "",
".nf": "",
".in": " ",
".ti": " ",
".sk": " ",
".ce": "\r",
}
rv = []
collecting = []
in_seq = False
for offset, c in enumerate(field):
if in_seq:
if c == self.esc:
in_seq = False
value = "".join(collecting)
collecting = []
if not value:
logger.warn(
"Error unescaping value [%s], empty sequence found at %d",
field,
offset,
)
continue
if app_map and value in app_map:
rv.append(app_map[value])
elif value in DEFAULT_MAP:
rv.append(DEFAULT_MAP[value])
elif value.startswith(".") and (
(app_map and value[:3] in app_map) or value[:3] in DEFAULT_MAP
):
# Substitution with a number of repetitions defined (2.10.6)
if app_map and value[:3] in app_map:
ch = app_map[value[:3]]
else:
ch = DEFAULT_MAP[value[:3]]
count = int(value[3:])
rv.append(ch * count)
elif (
value[0] == "C"
): # Convert to new Single Byte character set : 2.10.2
# Two HEX values, first value chooses the character set (ISO-IR), second gives the value
logger.warn(
"Error inline character sets [%s] not implemented, field [%s], offset [%s]",
value,
field,
offset,
)
elif (
value[0] == "M"
): # Switch to new Multi Byte character set : 2.10.2
# Three HEX values, first value chooses the character set (ISO-IR), rest give the value
logger.warn(
"Error inline character sets [%s] not implemented, field [%s], offset [%s]",
value,
field,
offset,
)
elif value[0] == "X": # Hex encoded Bytes: 2.10.5
value = value[1:]
try:
for off in range(0, len(value), 2):
rv.append(chr(int(value[off : off + 2], 16)))
except Exception:
logger.exception(
"Error decoding hex value [%s], field [%s], offset [%s]",
value,
field,
offset,
)
else:
logger.exception(
"Error decoding value [%s], field [%s], offset [%s]",
value,
field,
offset,
)
else:
collecting.append(c)
elif c == self.esc:
in_seq = True
else:
rv.append(str(c))
return "".join(rv)
def create_ack(
self, ack_code="AA", message_id=None, application=None, facility=None
):
"""
Create an hl7 ACK response :py:class:`hl7.Message`, per spec 2.9.2, for this message.
See http://www.hl7standards.com/blog/2007/02/01/ack-message-original-mode-acknowledgement/
``ack_code`` options are one of `AA` (Application Accept), `AR` (Application Reject),
`AE` (Application Error), `CA` (Commit Accept - Enhanced Mode),
`CR` (Commit Reject - Enhanced Mode), or `CE` (Commit Error - Enhanced Mode)
(see HL7 Table 0008 - Acknowledgment Code)
``message_id`` control message ID for ACK, defaults to unique generated ID
``application`` name of sending application, defaults to receiving application of message
``facility`` name of sending facility, defaults to receiving facility of message
"""
source_msh = self.segment("MSH")
msh = self.create_segment([self.create_field(["MSH"])])
msa = self.create_segment([self.create_field(["MSA"])])
ack = self.create_message([msh, msa])
ack.assign_field(str(source_msh(1)), "MSH", 1, 1)
ack.assign_field(str(source_msh(2)), "MSH", 1, 2)
# Sending application is source receving application
ack.assign_field(
str(application) if application is not None else str(source_msh(5)),
"MSH",
1,
3,
)
# Sending facility is source receving facility
ack.assign_field(
str(facility) if facility is not None else str(source_msh(6)), "MSH", 1, 4
)
# Receiving application is source sending application
ack.assign_field(str(source_msh(3)), "MSH", 1, 5)
# Receiving facility is source sending facility
ack.assign_field(str(source_msh(4)), "MSH", 1, 6)
ack.assign_field(
str(datetime.datetime.utcnow().strftime("%Y%m%d%H%M%S")), "MSH", 1, 7
)
# Message type code
ack.assign_field("ACK", "MSH", 1, 9, 1, 1)
# Copy trigger event from source
ack.assign_field(str(source_msh(9)(1)(2)), "MSH", 1, 9, 1, 2)
ack.assign_field("ACK", "MSH", 1, 9, 1, 3)
ack.assign_field(
message_id if message_id is not None else generate_message_control_id(),
"MSH",
1,
10,
)
ack.assign_field(str(source_msh(11)), "MSH", 1, 11)
ack.assign_field(str(source_msh(12)), "MSH", 1, 12)
ack.assign_field(str(ack_code), "MSA", 1, 1)
ack.assign_field(str(source_msh(10)), "MSA", 1, 2)
return ack
def __str__(self):
"""Join a the child containers into a single string, separated
by the self.separator. This method acts recursively, calling
the children's __unicode__ method. Thus ``unicode()`` is the
approriate method for turning the python-hl7 representation of
HL7 into a standard string.
>>> str(hl7.parse(message)) == message
True
"""
# Per spec, Message Construction Rules, Section 2.6 (v2.8), Message ends
# with the carriage return
return super(Message, self).__str__() + self.separator
class Segment(Container):
"""Second level of an HL7 message, which represents an HL7 Segment.
Traditionally this is a line of a message that ends with a carriage
return and is separated by pipes. It contains a list of
:py:class:`hl7.Field` instances.
"""
def _adjust_index(self, index):
# First element is the segment name, so we don't need to adjust to get 1-based
return index
def __str__(self):
if str(self[0]) in ["MSH", "FHS", "BHS"]:
return (
str(self[0])
+ str(self[1])
+ str(self[2])
+ str(self[1])
+ self.separator.join((str(x) for x in self[3:]))
)
return super(Segment, self).__str__()
class Field(Container):
"""Third level of an HL7 message, that traditionally is surrounded
by pipes and separated by carets. It contains a list of strings
or :py:class:`hl7.Repetition` instances.
"""
class Repetition(Container):
"""Fourth level of an HL7 message. A field can repeat.
It contains a list of strings or :py:class:`hl7.Component` instances.
"""
class Component(Container):
"""Fifth level of an HL7 message. A component is a composite datatypes.
It contains a list of string sub-components.
"""
class Factory(object):
"""Factory used to create each type of Container.
A subclass can be used to create specialized subclasses of each container.
"""
create_file = File #: Create an instance of :py:class:`hl7.File`
create_batch = Batch #: Create an instance of :py:class:`hl7.Batch`
create_message = Message #: Create an instance of :py:class:`hl7.Message`
create_segment = Segment #: Create an instance of :py:class:`hl7.Segment`
create_field = Field #: Create an instance of :py:class:`hl7.Field`
create_repetition = Repetition #: Create an instance of :py:class:`hl7.Repetition`
create_component = Component #: Create an instance of :py:class:`hl7.Component`
python-hl7-0.4.2/hl7/datatypes.py 0000664 0000000 0000000 00000004245 14012562772 0016603 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
import datetime
import math
import re
DTM_TZ_RE = re.compile(r"(\d+(?:\.\d+)?)(?:([+-]\d{2})(\d{2}))?")
class _UTCOffset(datetime.tzinfo):
"""Fixed offset timezone from UTC."""
def __init__(self, minutes):
"""``minutes`` is a offset from UTC, negative for west of UTC"""
self.minutes = minutes
def utcoffset(self, dt):
return datetime.timedelta(minutes=self.minutes)
def tzname(self, dt):
minutes = abs(self.minutes)
return "{0}{1:02}{2:02}".format(
"-" if self.minutes < 0 else "+", minutes // 60, minutes % 60
)
def dst(self, dt):
return datetime.timedelta(0)
def parse_datetime(value):
"""Parse hl7 DTM string ``value`` :py:class:`datetime.datetime`.
``value`` is of the format YYYY[MM[DD[HH[MM[SS[.S[S[S[S]]]]]]]]][+/-HHMM]
or a ValueError will be raised.
:rtype: :py:;class:`datetime.datetime`
"""
if not value:
return None
# Split off optional timezone
dt_match = DTM_TZ_RE.match(value)
if not dt_match:
raise ValueError("Malformed HL7 datetime {0}".format(value))
dtm = dt_match.group(1)
tzh = dt_match.group(2)
tzm = dt_match.group(3)
if tzh and tzm:
minutes = int(tzh) * 60
minutes += math.copysign(int(tzm), minutes)
tzinfo = _UTCOffset(minutes)
else:
tzinfo = None
precision = len(dtm)
if precision >= 4:
year = int(dtm[0:4])
else:
raise ValueError("Malformed HL7 datetime {0}".format(value))
if precision >= 6:
month = int(dtm[4:6])
else:
month = 1
if precision >= 8:
day = int(dtm[6:8])
else:
day = 1
if precision >= 10:
hour = int(dtm[8:10])
else:
hour = 0
if precision >= 12:
minute = int(dtm[10:12])
else:
minute = 0
if precision >= 14:
delta = datetime.timedelta(seconds=float(dtm[12:]))
second = delta.seconds
microsecond = delta.microseconds
else:
second = 0
microsecond = 0
return datetime.datetime(
year, month, day, hour, minute, second, microsecond, tzinfo=tzinfo
)
python-hl7-0.4.2/hl7/exceptions.py 0000664 0000000 0000000 00000000400 14012562772 0016753 0 ustar 00root root 0000000 0000000 class HL7Exception(Exception):
pass
class MalformedSegmentException(HL7Exception):
pass
class MalformedBatchException(HL7Exception):
pass
class MalformedFileException(HL7Exception):
pass
class ParseException(HL7Exception):
pass
python-hl7-0.4.2/hl7/mllp/ 0000775 0000000 0000000 00000000000 14012562772 0015172 5 ustar 00root root 0000000 0000000 python-hl7-0.4.2/hl7/mllp/__init__.py 0000664 0000000 0000000 00000000661 14012562772 0017306 0 ustar 00root root 0000000 0000000 from .exceptions import InvalidBlockError
from .streams import (
HL7StreamProtocol,
HL7StreamReader,
HL7StreamWriter,
MLLPStreamReader,
MLLPStreamWriter,
open_hl7_connection,
start_hl7_server,
)
__all__ = [
"open_hl7_connection",
"start_hl7_server",
"HL7StreamProtocol",
"HL7StreamReader",
"HL7StreamWriter",
"MLLPStreamReader",
"MLLPStreamWriter",
"InvalidBlockError",
]
python-hl7-0.4.2/hl7/mllp/exceptions.py 0000664 0000000 0000000 00000000145 14012562772 0017725 0 ustar 00root root 0000000 0000000 class InvalidBlockError(Exception):
"""An MLLP Block was received that violates MLLP protocol"""
python-hl7-0.4.2/hl7/mllp/streams.py 0000664 0000000 0000000 00000025430 14012562772 0017226 0 ustar 00root root 0000000 0000000 import warnings
from asyncio import (
LimitOverrunError,
StreamReader,
StreamReaderProtocol,
StreamWriter,
get_event_loop,
iscoroutine,
)
from asyncio.streams import _DEFAULT_LIMIT
from hl7.mllp.exceptions import InvalidBlockError
from hl7.parser import parse as hl7_parse
START_BLOCK = b"\x0b"
END_BLOCK = b"\x1c"
CARRIAGE_RETURN = b"\x0d"
async def open_hl7_connection(
host=None,
port=None,
*,
loop=None,
limit=_DEFAULT_LIMIT,
encoding=None,
encoding_errors=None,
**kwds
):
"""A wrapper for `loop.create_connection()` returning a (reader, writer) pair.
The reader returned is a :py:class:`hl7.mllp.HL7StreamReader` instance; the writer is a
:py:class:`hl7.mllp.HL7StreamWriter` instance.
The arguments are all the usual arguments to create_connection()
except `protocol_factory`; most common are positional `host` and `port`,
with various optional keyword arguments following.
Additional optional keyword arguments are `loop` (to set the event loop
instance to use), `limit` (to set the buffer limit passed to the
:py:class:`hl7.mllp.HL7StreamReader`), `encoding` (to set the encoding on the :py:class:`hl7.mllp.HL7StreamReader`
and :py:class:`hl7.mllp.HL7StreamWriter`) and `encoding_errors` (to set the encoding_errors on the :py:class:`hl7.mllp.HL7StreamReader`
and :py:class:`hl7.mllp.HL7StreamWriter`).
"""
if loop is None:
loop = get_event_loop()
else:
warnings.warn(
"The loop argument is deprecated since Python 3.8, "
"and scheduled for removal in Python 3.10.",
DeprecationWarning,
stacklevel=2,
)
reader = HL7StreamReader(
limit=limit, loop=loop, encoding=encoding, encoding_errors=encoding_errors
)
protocol = HL7StreamProtocol(
reader, loop=loop, encoding=encoding, encoding_errors=encoding_errors
)
transport, _ = await loop.create_connection(lambda: protocol, host, port, **kwds)
writer = HL7StreamWriter(
transport, protocol, reader, loop, encoding, encoding_errors
)
return reader, writer
async def start_hl7_server(
client_connected_cb,
host=None,
port=None,
*,
loop=None,
limit=_DEFAULT_LIMIT,
encoding=None,
encoding_errors=None,
**kwds
):
"""Start a socket server, call back for each client connected.
The first parameter, `client_connected_cb`, takes two parameters:
`client_reader`, `client_writer`. `client_reader` is a
:py:class:`hl7.mllp.HL7StreamReader` object, while `client_writer`
is a :py:class:`hl7.mllp.HL7StreamWriter` object. This
parameter can either be a plain callback function or a coroutine;
if it is a coroutine, it will be automatically converted into a
`Task`.
The rest of the arguments are all the usual arguments to
`loop.create_server()` except `protocol_factory`; most common are
positional `host` and `port`, with various optional keyword arguments
following.
The return value is the same as `loop.create_server()`.
Additional optional keyword arguments are `loop` (to set the event loop
instance to use) and `limit` (to set the buffer limit passed to the
StreamReader).
The return value is the same as `loop.create_server()`, i.e. a
`Server` object which can be used to stop the service.
"""
if loop is None:
loop = get_event_loop()
else:
warnings.warn(
"The loop argument is deprecated since Python 3.8, "
"and scheduled for removal in Python 3.10.",
DeprecationWarning,
stacklevel=2,
)
def factory():
reader = HL7StreamReader(
limit=limit, loop=loop, encoding=encoding, encoding_errors=encoding_errors
)
protocol = HL7StreamProtocol(
reader,
client_connected_cb,
loop=loop,
encoding=encoding,
encoding_errors=encoding_errors,
)
return protocol
return await loop.create_server(factory, host, port, **kwds)
class MLLPStreamReader(StreamReader):
def __init__(self, limit=_DEFAULT_LIMIT, loop=None):
super().__init__(limit, loop)
async def readblock(self):
"""Read a chunk of data from the stream until the block termination
separator (b'\x1c\x0d') are found.
On success, the data and separator will be removed from the
internal buffer (consumed). Returned data will not include the
separator at the end or the MLLP start block character (b'\x0b') at the
beginning.
Configured stream limit is used to check result. Limit sets the
maximal length of data that can be returned, not counting the
separator.
If an EOF occurs and the complete separator is still not found,
an IncompleteReadError exception will be raised, and the internal
buffer will be reset. The IncompleteReadError.partial attribute
may contain the separator partially.
If limit is reached, ValueError will be raised. In that case, if
block termination separator was found, complete line including separator
will be removed from internal buffer. Else, internal buffer will be cleared. Limit is
compared against part of the line without separator.
If the block is invalid (missing required start block character) and InvalidBlockError
will be raised.
If stream was paused, this function will automatically resume it if
needed.
"""
sep = END_BLOCK + CARRIAGE_RETURN
seplen = len(sep)
try:
block = await self.readuntil(sep)
except LimitOverrunError as loe:
if self._buffer.startswith(sep, loe.consumed):
del self._buffer[: loe.consumed + seplen]
else:
self._buffer.clear()
self._maybe_resume_transport()
raise ValueError(loe.args[0])
if not block or block[0:1] != START_BLOCK:
raise InvalidBlockError(
"Block does not begin with Start Block character "
)
return block[1:-2]
class MLLPStreamWriter(StreamWriter):
def __init__(self, transport, protocol, reader, loop):
super().__init__(transport, protocol, reader, loop)
def writeblock(self, data):
"""Write a block of data to the stream,
encapsulating the block with b'\x0b' at the beginning
and b'\x1c\x0d' at the end.
"""
self.write(START_BLOCK + data + END_BLOCK + CARRIAGE_RETURN)
class HL7StreamProtocol(StreamReaderProtocol):
def __init__(
self,
stream_reader,
client_connected_cb=None,
loop=None,
encoding=None,
encoding_errors=None,
):
super().__init__(stream_reader, client_connected_cb, loop)
self._encoding = encoding
self._encoding_errors = encoding_errors
def connection_made(self, transport):
if self._reject_connection:
context = {
"message": (
"An open stream was garbage collected prior to "
"establishing network connection; "
'call "stream.close()" explicitly.'
)
}
if self._source_traceback:
context["source_traceback"] = self._source_traceback
self._loop.call_exception_handler(context)
transport.abort()
return
self._transport = transport
reader = self._stream_reader
if reader is not None:
reader.set_transport(transport)
self._over_ssl = transport.get_extra_info("sslcontext") is not None
if self._client_connected_cb is not None:
self._stream_writer = HL7StreamWriter(
transport,
self,
reader,
self._loop,
self._encoding,
self._encoding_errors,
)
res = self._client_connected_cb(reader, self._stream_writer)
if iscoroutine(res):
self._loop.create_task(res)
self._strong_reader = None
class HL7StreamReader(MLLPStreamReader):
def __init__(
self, limit=_DEFAULT_LIMIT, loop=None, encoding=None, encoding_errors=None
):
super().__init__(limit=limit, loop=loop)
self.encoding = encoding
self.encoding_errors = encoding_errors
@property
def encoding(self):
return self._encoding
@encoding.setter
def encoding(self, encoding):
if encoding and not isinstance(encoding, str):
raise TypeError("encoding must be a str or None")
self._encoding = encoding or "ascii"
@property
def encoding_errors(self):
return self._encoding_errors
@encoding_errors.setter
def encoding_errors(self, encoding_errors):
if encoding_errors and not isinstance(encoding_errors, str):
raise TypeError("encoding_errors must be a str or None")
self._encoding_errors = encoding_errors or "strict"
async def readmessage(self):
"""Reads a full HL7 message from the stream.
This will return an :py:class:`hl7.Message`.
If `limit` is reached, `ValueError` will be raised. In that case, if
block termination separator was found, complete line including separator
will be removed from internal buffer. Else, internal buffer will be cleared. Limit is
compared against part of the line without separator.
If an invalid MLLP block is encountered, :py:class:`hl7.mllp.InvalidBlockError` will be
raised.
"""
block = await self.readblock()
return hl7_parse(block.decode(self.encoding, self.encoding_errors))
class HL7StreamWriter(MLLPStreamWriter):
def __init__(
self, transport, protocol, reader, loop, encoding=None, encoding_errors=None
):
super().__init__(transport, protocol, reader, loop)
self.encoding = encoding
self.encoding_errors = encoding_errors
@property
def encoding(self):
return self._encoding
@encoding.setter
def encoding(self, encoding):
if encoding and not isinstance(encoding, str):
raise TypeError("encoding must be a str or None")
self._encoding = encoding or "ascii"
@property
def encoding_errors(self):
return self._encoding_errors
@encoding_errors.setter
def encoding_errors(self, encoding_errors):
if encoding_errors and not isinstance(encoding_errors, str):
raise TypeError("encoding_errors must be a str or None")
self._encoding_errors = encoding_errors or "strict"
def writemessage(self, message):
"""Writes an :py:class:`hl7.Message` to the stream.
"""
self.writeblock(str(message).encode(self.encoding, self.encoding_errors))
python-hl7-0.4.2/hl7/parser.py 0000664 0000000 0000000 00000036014 14012562772 0016100 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
from string import whitespace
from .containers import Factory
from .exceptions import ParseException
from .util import isbatch, isfile, ishl7
_HL7_WHITESPACE = whitespace.replace("\r", "")
def parse_hl7(line, encoding="utf-8", factory=Factory):
"""Returns a instance of the :py:class:`hl7.Message`, :py:class:`hl7.Batch`
or :py:class:`hl7.File` that allows indexed access to the data elements or
messages or batches respectively.
A custom :py:class:`hl7.Factory` subclass can be passed in to be used when
constructing the message/batch/file and it's components.
.. note::
HL7 usually contains only ASCII, but can use other character
sets (HL7 Standards Document, Section 1.7.1), however as of v2.8,
UTF-8 is the preferred character set [#]_.
python-hl7 works on Python unicode strings. :py:func:`hl7.parse_hl7`
will accept unicode string or will attempt to convert bytestrings
into unicode strings using the optional ``encoding`` parameter.
``encoding`` defaults to UTF-8, so no work is needed for bytestrings
in UTF-8, but for other character sets like 'cp1252' or 'latin1',
``encoding`` must be set appropriately.
>>> h = hl7.parse_hl7(message)
To decode a non-UTF-8 byte string::
hl7.parse_hl7(message, encoding='latin1')
:rtype: :py:class:`hl7.Message` | :py:class:`hl7.Batch` | :py:class:`hl7.File`
.. [#] http://wiki.hl7.org/index.php?title=Character_Set_used_in_v2_messages
"""
# Ensure we are working with unicode data, decode the bytestring
# if needed
if isinstance(line, bytes):
line = line.decode(encoding)
# If it is an HL7 message, parse as normal
if ishl7(line):
return parse(line, encoding=encoding, factory=factory)
# If we have a batch, then parse the batch
elif isbatch(line):
return parse_batch(line, encoding=encoding, factory=factory)
# If we have a file, parse the HL7 file
elif isfile(line):
return parse_file(line, encoding=encoding, factory=factory)
# Not an HL7 message
raise ValueError("line is not HL7")
def parse(lines, encoding="utf-8", factory=Factory):
"""Returns a instance of the :py:class:`hl7.Message` that allows
indexed access to the data elements.
A custom :py:class:`hl7.Factory` subclass can be passed in to be used when
constructing the message and it's components.
.. note::
HL7 usually contains only ASCII, but can use other character
sets (HL7 Standards Document, Section 1.7.1), however as of v2.8,
UTF-8 is the preferred character set [#]_.
python-hl7 works on Python unicode strings. :py:func:`hl7.parse`
will accept unicode string or will attempt to convert bytestrings
into unicode strings using the optional ``encoding`` parameter.
``encoding`` defaults to UTF-8, so no work is needed for bytestrings
in UTF-8, but for other character sets like 'cp1252' or 'latin1',
``encoding`` must be set appropriately.
>>> h = hl7.parse(message)
To decode a non-UTF-8 byte string::
hl7.parse(message, encoding='latin1')
:rtype: :py:class:`hl7.Message`
.. [#] http://wiki.hl7.org/index.php?title=Character_Set_used_in_v2_messages
"""
# Ensure we are working with unicode data, decode the bytestring
# if needed
if isinstance(lines, bytes):
lines = lines.decode(encoding)
# Strip out unnecessary whitespace
strmsg = lines.strip()
# The method for parsing the message
plan = create_parse_plan(strmsg, factory)
# Start spliting the methods based upon the ParsePlan
return _split(strmsg, plan)
def _create_batch(batch, messages, encoding, factory):
"""Creates a :py:class:`hl7.Batch`
"""
kwargs = {
"separator": "\r",
"sequence": [
parse(message, encoding=encoding, factory=factory) for message in messages
],
}
# If the BHS/BTS were present, use those to set up the batch
# otherwise default
if batch:
batch = parse(batch, encoding=encoding, factory=factory)
kwargs["esc"] = batch.esc
kwargs["separators"] = batch.separators
kwargs["factory"] = batch.factory
parsed = factory.create_batch(**kwargs)
# If the BHS/BTS were present then set them
if batch:
parsed.header = batch.segment("BHS")
try:
parsed.trailer = batch.segment("BTS")
except KeyError:
parsed.trailer = parsed.create_segment([parsed.create_field(["BTS"])])
return parsed
def parse_batch(lines, encoding="utf-8", factory=Factory):
"""Returns a instance of a :py:class:`hl7.Batch`
that allows indexed access to the messages.
A custom :py:class:`hl7.Factory` subclass can be passed in to be used when
constructing the batch and it's components.
.. note::
HL7 usually contains only ASCII, but can use other character
sets (HL7 Standards Document, Section 1.7.1), however as of v2.8,
UTF-8 is the preferred character set [#]_.
python-hl7 works on Python unicode strings. :py:func:`hl7.parse_batch`
will accept unicode string or will attempt to convert bytestrings
into unicode strings using the optional ``encoding`` parameter.
``encoding`` defaults to UTF-8, so no work is needed for bytestrings
in UTF-8, but for other character sets like 'cp1252' or 'latin1',
``encoding`` must be set appropriately.
>>> h = hl7.parse_batch(message)
To decode a non-UTF-8 byte string::
hl7.parse_batch(message, encoding='latin1')
:rtype: :py:class:`hl7.Batch`
.. [#] http://wiki.hl7.org/index.php?title=Character_Set_used_in_v2_messages
"""
# Ensure we are working with unicode data, decode the bytestring
# if needed
if isinstance(lines, bytes):
lines = lines.decode(encoding)
batch = None
messages = []
# Split the batch into lines, retaining the ends
for line in lines.strip(_HL7_WHITESPACE).splitlines(keepends=True):
# strip out all whitespace MINUS the '\r'
line = line.strip(_HL7_WHITESPACE)
if line[:3] == "BHS":
if batch:
raise ParseException("Batch cannot have more than one BHS segment")
batch = line
elif line[:3] == "BTS":
if not batch or "\rBTS" in batch:
continue
batch += line
elif line[:3] == "MSH":
messages.append(line)
else:
if not messages:
raise ParseException(
"Segment received before message header {}".format(line)
)
messages[-1] += line
return _create_batch(batch, messages, encoding, factory)
def _create_file(file, batches, encoding, factory):
kwargs = {
"separator": "\r",
"sequence": [
_create_batch(batch[0], batch[1], encoding, factory) for batch in batches
],
}
# If the FHS/FTS are present, use them to set up the file
if file:
file = parse(file, encoding=encoding, factory=factory)
kwargs["esc"] = file.esc
kwargs["separators"] = file.separators
kwargs["factory"] = file.factory
parsed = factory.create_file(**kwargs)
# If the FHS/FTS are present, add them
if file:
parsed.header = file.segment("FHS")
try:
parsed.trailer = file.segment("FTS")
except KeyError:
parsed.trailer = parsed.create_segment([parsed.create_field(["FTS"])])
return parsed
def parse_file(lines, encoding="utf-8", factory=Factory): # noqa: C901
"""Returns a instance of the :py:class:`hl7.File` that allows
indexed access to the batches.
A custom :py:class:`hl7.Factory` subclass can be passed in to be used when
constructing the file and it's components.
.. note::
HL7 usually contains only ASCII, but can use other character
sets (HL7 Standards Document, Section 1.7.1), however as of v2.8,
UTF-8 is the preferred character set [#]_.
python-hl7 works on Python unicode strings. :py:func:`hl7.parse_file`
will accept unicode string or will attempt to convert bytestrings
into unicode strings using the optional ``encoding`` parameter.
``encoding`` defaults to UTF-8, so no work is needed for bytestrings
in UTF-8, but for other character sets like 'cp1252' or 'latin1',
``encoding`` must be set appropriately.
>>> h = hl7.parse_file(message)
To decode a non-UTF-8 byte string::
hl7.parse_file(message, encoding='latin1')
:rtype: :py:class:`hl7.File`
.. [#] http://wiki.hl7.org/index.php?title=Character_Set_used_in_v2_messages
"""
# Ensure we are working with unicode data, decode the bytestring
# if needed
if isinstance(lines, bytes):
lines = lines.decode(encoding)
file = None
batches = []
messages = []
in_batch = False
# Split the file into lines, reatining the ends
for line in lines.strip(_HL7_WHITESPACE).splitlines(keepends=True):
# strip out all whitespace MINUS the '\r'
line = line.strip(_HL7_WHITESPACE)
if line[:3] == "FHS":
if file:
raise ParseException("File cannot have more than one FHS segment")
file = line
elif line[:3] == "FTS":
if not file or "\rFTS" in file:
continue
file += line
elif line[:3] == "BHS":
if in_batch:
raise ParseException("Batch cannot have more than one BHS segment")
batches.append([line, []])
in_batch = True
elif line[:3] == "BTS":
if not in_batch:
continue
batches[-1][0] += line
in_batch = False
elif line[:3] == "MSH":
if in_batch:
batches[-1][1].append(line)
else: # Messages outside of a batch go into the "default" batch
messages.append(line)
else:
if in_batch:
if not batches[-1][1]:
raise ParseException(
"Segment received before message header {}".format(line)
)
batches[-1][1][-1] += line
else:
if not messages:
raise ParseException(
"Segment received before message header {}".format(line)
)
messages[-1] += line
if messages: # add the default batch, if we have one
batches.append([None, messages])
return _create_file(file, batches, encoding, factory)
def _split(text, plan):
"""Recursive function to split the *text* into an n-deep list,
according to the :py:class:`hl7._ParsePlan`.
"""
# Base condition, if we have used up all the plans
if not plan:
return text
if not plan.applies(text):
return plan.container([text])
# Parsing of the first segment is awkward because it contains
# the separator characters in a field
if plan.containers[0] == plan.factory.create_segment and text[:3] in [
"MSH",
"BHS",
"FHS",
]:
seg = text[:3]
sep0 = text[3]
sep_end_off = text.find(sep0, 4)
seps = text[4:sep_end_off]
text = text[sep_end_off + 1 :]
data = [
plan.factory.create_field("", [seg]),
plan.factory.create_field("", [sep0]),
plan.factory.create_field(sep0, [seps]),
]
else:
data = []
if text:
data = data + [_split(x, plan.next()) for x in text.split(plan.separator)]
# Return the instance of the current message part according
# to the plan
return plan.container(data)
def create_parse_plan(strmsg, factory=Factory):
"""Creates a plan on how to parse the HL7 message according to
the details stored within the message.
"""
# We will always use a carriage return to separate segments
separators = ["\r"]
# Extract the rest of the separators. Defaults used if not present.
if strmsg[:3] not in ("MSH", "FHS", "BHS"):
raise ParseException(
"First segment is {}, must be one of MHS, FHS or BHS".format(strmsg[:3])
)
sep0 = strmsg[3]
seps = list(strmsg[3 : strmsg.find(sep0, 4)])
separators.append(seps[0])
if len(seps) > 2:
separators.append(seps[2]) # repetition separator
else:
separators.append("~") # repetition separator
if len(seps) > 1:
separators.append(seps[1]) # component separator
else:
separators.append("^") # component separator
if len(seps) > 4:
separators.append(seps[4]) # sub-component separator
else:
separators.append("&") # sub-component separator
if len(seps) > 3:
esc = seps[3]
else:
esc = "\\"
# The ordered list of containers to create
containers = [
factory.create_message,
factory.create_segment,
factory.create_field,
factory.create_repetition,
factory.create_component,
]
return _ParsePlan(separators, containers, esc, factory)
class _ParsePlan(object):
"""Details on how to parse an HL7 message. Typically this object
should be created via :func:`hl7.create_parse_plan`
"""
# field, component, repetition, escape, subcomponent
def __init__(self, separators, containers, esc, factory):
# TODO test to see performance implications of the assertion
# since we generate the ParsePlan, this should never be in
# invalid state
assert len(containers) == len(separators)
self.separators = separators
self.containers = containers
self.esc = esc
self.factory = factory
@property
def separator(self):
"""Return the current separator to use based on the plan."""
return self.separators[0]
def container(self, data):
"""Return an instance of the approriate container for the *data*
as specified by the current plan.
"""
return self.containers[0](
self.separator, data, self.esc, self.separators, self.factory
)
def next(self):
"""Generate the next level of the plan (essentially generates
a copy of this plan with the level of the container and the
seperator starting at the next index.
"""
if len(self.containers) > 1:
# Return a new instance of this class using the tails of
# the separators and containers lists. Use self.__class__()
# in case :class:`hl7.ParsePlan` is subclassed
return self.__class__(
self.separators[1:], self.containers[1:], self.esc, self.factory
)
# When we have no separators and containers left, return None,
# which indicates that we have nothing further.
return None
def applies(self, text):
"""return True if the separator or those if the children are in the text"""
for s in self.separators:
if text.find(s) >= 0:
return True
return False
python-hl7-0.4.2/hl7/util.py 0000664 0000000 0000000 00000004314 14012562772 0015557 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
import datetime
import logging
import random
import string
logger = logging.getLogger(__file__)
def ishl7(line):
"""Determines whether a *line* looks like an HL7 message.
This method only does a cursory check and does not fully
validate the message.
:rtype: bool
"""
# Prevent issues if the line is empty
return line and line.strip()[:3] == "MSH" and line.count("MSH") == 1
def isbatch(line):
"""
Batches are wrapped in BHS / BTS or have more than one
message
BHS = batch header segment
BTS = batch trailer segment
"""
return line and (
line.strip()[:3] == "BHS"
or (line.count("MSH") > 1 and line.strip()[:3] != "FHS")
)
def isfile(line):
"""
Files are wrapped in FHS / FTS, or may be a batch
FHS = file header segment
FTS = file trailer segment
"""
return line and (line.strip()[:3] == "FHS" or isbatch(line))
def split_file(hl7file):
"""
Given a file, split out the messages.
Does not do any validation on the message.
Throws away batch and file segments.
"""
rv = []
for line in hl7file.split("\r"):
line = line.strip()
if line[:3] in ["FHS", "BHS", "FTS", "BTS"]:
continue
if line[:3] == "MSH":
newmsg = [line]
rv.append(newmsg)
else:
if len(rv) == 0:
logger.error("Segment received before message header [%s]", line)
continue
rv[-1].append(line)
rv = ["\r".join(msg) for msg in rv]
for i, msg in enumerate(rv):
if not msg[-1] == "\r":
rv[i] = msg + "\r"
return rv
alphanumerics = string.ascii_uppercase + string.digits
def generate_message_control_id():
"""Generate a unique 20 character message id.
See http://www.hl7resources.com/Public/index.html?a55433.htm
"""
d = datetime.datetime.utcnow()
# Strip off the decade, ID only has to be unique for 3 years.
# So now we have a 16 char timestamp.
timestamp = d.strftime("%y%j%H%M%S%f")[1:]
# Add 4 chars of uniqueness
unique = "".join(random.sample(alphanumerics, 4))
return timestamp + unique
python-hl7-0.4.2/hl7/version.py 0000664 0000000 0000000 00000001333 14012562772 0016265 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
"""
Primary version number source.
Forth element can be 'dev' < 'a' < 'b' < 'rc' < 'final'. An empty 4th
element is equivalent to 'final'.
"""
VERSION = (0, 4, 2, "final")
def get_version():
"""Provide version number
Use verlib format [1]_:
N.N[.N]+[{a|b|c|rc}N[.N]+][.postN][.devN]
.. [1] http://www.python.org/dev/peps/pep-0386/
"""
main_version = "%s.%s.%s" % VERSION[0:3]
if len(VERSION) < 4:
return main_version
version_type = VERSION[3]
if not version_type or version_type == "final":
return main_version
elif version_type == "dev":
return "%s.dev" % main_version
else:
return "%s%s" % (main_version, version_type)
python-hl7-0.4.2/requirements.txt 0000664 0000000 0000000 00000000275 14012562772 0017024 0 ustar 00root root 0000000 0000000 # pip Requirements for developing python-hl7 (not required to use as a library)
tox==3.14.4
flake8==3.8.3
Sphinx==2.4.1
coverage==5.0.3
isort==4.3.21
black==19.10b0; python_version > "3.5"
python-hl7-0.4.2/setup.cfg 0000664 0000000 0000000 00000000555 14012562772 0015362 0 ustar 00root root 0000000 0000000 [flake8]
# http://flake8.readthedocs.org/en/latest/config.html#global
# E501 -- ignore long lines (still prefer 80 character limit)
ignore = E501
exclude = ._*
[bdist_wheel]
universal = 1
[isort]
# compatible settings for black
multi_line_output = 3
include_trailing_comma = true
force_grid_wrap = 0
use_parentheses = true
line_length = 88
known_first_party=hl7
python-hl7-0.4.2/setup.py 0000664 0000000 0000000 00000003231 14012562772 0015245 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
from setuptools import setup
# Avoid directly importing the module. Prevents potential circular
# references when dependency needs to be installed via setup.py, so it
# is not yet available to setup.py
exec(open("hl7/version.py").read())
setup(
name="hl7",
version=get_version(), # noqa
description="Python library parsing HL7 v2.x messages",
long_description="""
python-hl7 is a simple library for parsing messages of Health Level 7
(HL7) version 2.x into Python objects.
* Documentation: http://python-hl7.readthedocs.org
* Source Code: http://github.com/johnpaulett/python-hl7
""",
author="John Paulett",
author_email="john@paulett.org",
url="http://python-hl7.readthedocs.org",
license="BSD",
platforms=["POSIX", "Windows"],
keywords=[
"HL7",
"Health Level 7",
"healthcare",
"health care",
"medical record",
"mllp",
],
classifiers=[
"License :: OSI Approved :: BSD License",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"Intended Audience :: Healthcare Industry",
"Topic :: Communications",
"Topic :: Scientific/Engineering :: Medical Science Apps.",
"Topic :: Software Development :: Libraries :: Python Modules",
],
packages=["hl7", "hl7.mllp"],
install_requires=[],
test_suite="tests",
tests_require=[],
entry_points={"console_scripts": ["mllp_send=hl7.client:mllp_send",],},
zip_safe=True,
)
python-hl7-0.4.2/tests/ 0000775 0000000 0000000 00000000000 14012562772 0014676 5 ustar 00root root 0000000 0000000 python-hl7-0.4.2/tests/__init__.py 0000664 0000000 0000000 00000000000 14012562772 0016775 0 ustar 00root root 0000000 0000000 python-hl7-0.4.2/tests/backports/ 0000775 0000000 0000000 00000000000 14012562772 0016666 5 ustar 00root root 0000000 0000000 python-hl7-0.4.2/tests/backports/__init__.py 0000664 0000000 0000000 00000000000 14012562772 0020765 0 ustar 00root root 0000000 0000000 python-hl7-0.4.2/tests/backports/unittest/ 0000775 0000000 0000000 00000000000 14012562772 0020545 5 ustar 00root root 0000000 0000000 python-hl7-0.4.2/tests/backports/unittest/LICENSE 0000664 0000000 0000000 00000033252 14012562772 0021557 0 ustar 00root root 0000000 0000000 # From https://raw.githubusercontent.com/python/cpython/3.8/LICENSE
"""
A. HISTORY OF THE SOFTWARE
==========================
Python was created in the early 1990s by Guido van Rossum at Stichting
Mathematisch Centrum (CWI, see http://www.cwi.nl) in the Netherlands
as a successor of a language called ABC. Guido remains Python's
principal author, although it includes many contributions from others.
In 1995, Guido continued his work on Python at the Corporation for
National Research Initiatives (CNRI, see http://www.cnri.reston.va.us)
in Reston, Virginia where he released several versions of the
software.
In May 2000, Guido and the Python core development team moved to
BeOpen.com to form the BeOpen PythonLabs team. In October of the same
year, the PythonLabs team moved to Digital Creations, which became
Zope Corporation. In 2001, the Python Software Foundation (PSF, see
https://www.python.org/psf/) was formed, a non-profit organization
created specifically to own Python-related Intellectual Property.
Zope Corporation was a sponsoring member of the PSF.
All Python releases are Open Source (see http://www.opensource.org for
the Open Source Definition). Historically, most, but not all, Python
releases have also been GPL-compatible; the table below summarizes
the various releases.
Release Derived Year Owner GPL-
from compatible? (1)
0.9.0 thru 1.2 1991-1995 CWI yes
1.3 thru 1.5.2 1.2 1995-1999 CNRI yes
1.6 1.5.2 2000 CNRI no
2.0 1.6 2000 BeOpen.com no
1.6.1 1.6 2001 CNRI yes (2)
2.1 2.0+1.6.1 2001 PSF no
2.0.1 2.0+1.6.1 2001 PSF yes
2.1.1 2.1+2.0.1 2001 PSF yes
2.1.2 2.1.1 2002 PSF yes
2.1.3 2.1.2 2002 PSF yes
2.2 and above 2.1.1 2001-now PSF yes
Footnotes:
(1) GPL-compatible doesn't mean that we're distributing Python under
the GPL. All Python licenses, unlike the GPL, let you distribute
a modified version without making your changes open source. The
GPL-compatible licenses make it possible to combine Python with
other software that is released under the GPL; the others don't.
(2) According to Richard Stallman, 1.6.1 is not GPL-compatible,
because its license has a choice of law clause. According to
CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1
is "not incompatible" with the GPL.
Thanks to the many outside volunteers who have worked under Guido's
direction to make these releases possible.
B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON
===============================================================
Python software and documentation are licensed under the
Python Software Foundation License Version 2.
Starting with Python 3.8.6, examples, recipes, and other code in
the documentation are dual licensed under the PSF License Version 2
and the Zero-Clause BSD license.
Some software incorporated into Python is under different licenses.
The licenses are listed with code falling under that license.
PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
--------------------------------------------
1. This LICENSE AGREEMENT is between the Python Software Foundation
("PSF"), and the Individual or Organization ("Licensee") accessing and
otherwise using this software ("Python") in source or binary form and
its associated documentation.
2. Subject to the terms and conditions of this License Agreement, PSF hereby
grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
analyze, test, perform and/or display publicly, prepare derivative works,
distribute, and otherwise use Python alone or in any derivative version,
provided, however, that PSF's License Agreement and PSF's notice of copyright,
i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Python Software Foundation;
All Rights Reserved" are retained in Python alone or in any derivative version
prepared by Licensee.
3. In the event Licensee prepares a derivative work that is based on
or incorporates Python or any part thereof, and wants to make
the derivative work available to others as provided herein, then
Licensee hereby agrees to include in any such work a brief summary of
the changes made to Python.
4. PSF is making Python available to Licensee on an "AS IS"
basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
INFRINGE ANY THIRD PARTY RIGHTS.
5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
6. This License Agreement will automatically terminate upon a material
breach of its terms and conditions.
7. Nothing in this License Agreement shall be deemed to create any
relationship of agency, partnership, or joint venture between PSF and
Licensee. This License Agreement does not grant permission to use PSF
trademarks or trade name in a trademark sense to endorse or promote
products or services of Licensee, or any third party.
8. By copying, installing or otherwise using Python, Licensee
agrees to be bound by the terms and conditions of this License
Agreement.
BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0
-------------------------------------------
BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1
1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an
office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the
Individual or Organization ("Licensee") accessing and otherwise using
this software in source or binary form and its associated
documentation ("the Software").
2. Subject to the terms and conditions of this BeOpen Python License
Agreement, BeOpen hereby grants Licensee a non-exclusive,
royalty-free, world-wide license to reproduce, analyze, test, perform
and/or display publicly, prepare derivative works, distribute, and
otherwise use the Software alone or in any derivative version,
provided, however, that the BeOpen Python License is retained in the
Software, alone or in any derivative version prepared by Licensee.
3. BeOpen is making the Software available to Licensee on an "AS IS"
basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT
INFRINGE ANY THIRD PARTY RIGHTS.
4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE
SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS
AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY
DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
5. This License Agreement will automatically terminate upon a material
breach of its terms and conditions.
6. This License Agreement shall be governed by and interpreted in all
respects by the law of the State of California, excluding conflict of
law provisions. Nothing in this License Agreement shall be deemed to
create any relationship of agency, partnership, or joint venture
between BeOpen and Licensee. This License Agreement does not grant
permission to use BeOpen trademarks or trade names in a trademark
sense to endorse or promote products or services of Licensee, or any
third party. As an exception, the "BeOpen Python" logos available at
http://www.pythonlabs.com/logos.html may be used according to the
permissions granted on that web page.
7. By copying, installing or otherwise using the software, Licensee
agrees to be bound by the terms and conditions of this License
Agreement.
CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1
---------------------------------------
1. This LICENSE AGREEMENT is between the Corporation for National
Research Initiatives, having an office at 1895 Preston White Drive,
Reston, VA 20191 ("CNRI"), and the Individual or Organization
("Licensee") accessing and otherwise using Python 1.6.1 software in
source or binary form and its associated documentation.
2. Subject to the terms and conditions of this License Agreement, CNRI
hereby grants Licensee a nonexclusive, royalty-free, world-wide
license to reproduce, analyze, test, perform and/or display publicly,
prepare derivative works, distribute, and otherwise use Python 1.6.1
alone or in any derivative version, provided, however, that CNRI's
License Agreement and CNRI's notice of copyright, i.e., "Copyright (c)
1995-2001 Corporation for National Research Initiatives; All Rights
Reserved" are retained in Python 1.6.1 alone or in any derivative
version prepared by Licensee. Alternately, in lieu of CNRI's License
Agreement, Licensee may substitute the following text (omitting the
quotes): "Python 1.6.1 is made available subject to the terms and
conditions in CNRI's License Agreement. This Agreement together with
Python 1.6.1 may be located on the Internet using the following
unique, persistent identifier (known as a handle): 1895.22/1013. This
Agreement may also be obtained from a proxy server on the Internet
using the following URL: http://hdl.handle.net/1895.22/1013".
3. In the event Licensee prepares a derivative work that is based on
or incorporates Python 1.6.1 or any part thereof, and wants to make
the derivative work available to others as provided herein, then
Licensee hereby agrees to include in any such work a brief summary of
the changes made to Python 1.6.1.
4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS"
basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT
INFRINGE ANY THIRD PARTY RIGHTS.
5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1,
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
6. This License Agreement will automatically terminate upon a material
breach of its terms and conditions.
7. This License Agreement shall be governed by the federal
intellectual property law of the United States, including without
limitation the federal copyright law, and, to the extent such
U.S. federal law does not apply, by the law of the Commonwealth of
Virginia, excluding Virginia's conflict of law provisions.
Notwithstanding the foregoing, with regard to derivative works based
on Python 1.6.1 that incorporate non-separable material that was
previously distributed under the GNU General Public License (GPL), the
law of the Commonwealth of Virginia shall govern this License
Agreement only as to issues arising under or with respect to
Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this
License Agreement shall be deemed to create any relationship of
agency, partnership, or joint venture between CNRI and Licensee. This
License Agreement does not grant permission to use CNRI trademarks or
trade name in a trademark sense to endorse or promote products or
services of Licensee, or any third party.
8. By clicking on the "ACCEPT" button where indicated, or by copying,
installing or otherwise using Python 1.6.1, Licensee agrees to be
bound by the terms and conditions of this License Agreement.
ACCEPT
CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2
--------------------------------------------------
Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam,
The Netherlands. All rights reserved.
Permission to use, copy, modify, and distribute this software and its
documentation for any purpose and without fee is hereby granted,
provided that the above copyright notice appear in all copies and that
both that copyright notice and this permission notice appear in
supporting documentation, and that the name of Stichting Mathematisch
Centrum or CWI not be used in advertising or publicity pertaining to
distribution of the software without specific, written prior
permission.
STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO
THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE
FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
ZERO-CLAUSE BSD LICENSE FOR CODE IN THE PYTHON DOCUMENTATION
----------------------------------------------------------------------
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
""" python-hl7-0.4.2/tests/backports/unittest/__init__.py 0000664 0000000 0000000 00000000000 14012562772 0022644 0 ustar 00root root 0000000 0000000 python-hl7-0.4.2/tests/backports/unittest/async_case.py 0000664 0000000 0000000 00000014124 14012562772 0023231 0 ustar 00root root 0000000 0000000 # TODO Remove once 3.7 support is dropped
# Backport of Python 3.8's new unittest.IsolatedAsyncioTestCase
# From https://github.com/python/cpython/blob/3.8/Lib/unittest/async_case.py
import asyncio
import inspect
import sys
from .case import TestCase
# asyncio.all_tasks added in 3.7, use backport for older code
if sys.version_info.major <= 3 and sys.version_info.minor < 7:
all_tasks = asyncio.Task.all_tasks
else:
all_tasks = asyncio.all_tasks
class IsolatedAsyncioTestCase(TestCase):
# Names intentionally have a long prefix
# to reduce a chance of clashing with user-defined attributes
# from inherited test case
#
# The class doesn't call loop.run_until_complete(self.setUp()) and family
# but uses a different approach:
# 1. create a long-running task that reads self.setUp()
# awaitable from queue along with a future
# 2. await the awaitable object passing in and set the result
# into the future object
# 3. Outer code puts the awaitable and the future object into a queue
# with waiting for the future
# The trick is necessary because every run_until_complete() call
# creates a new task with embedded ContextVar context.
# To share contextvars between setUp(), test and tearDown() we need to execute
# them inside the same task.
# Note: the test case modifies event loop policy if the policy was not instantiated
# yet.
# asyncio.get_event_loop_policy() creates a default policy on demand but never
# returns None
# I believe this is not an issue in user level tests but python itself for testing
# should reset a policy in every test module
# by calling asyncio.set_event_loop_policy(None) in tearDownModule()
def __init__(self, methodName="runTest"):
super().__init__(methodName)
self._asyncioTestLoop = None
self._asyncioCallsQueue = None
async def asyncSetUp(self):
pass
async def asyncTearDown(self):
pass
def addAsyncCleanup(self, func, *args, **kwargs):
# A trivial trampoline to addCleanup()
# the function exists because it has a different semantics
# and signature:
# addCleanup() accepts regular functions
# but addAsyncCleanup() accepts coroutines
#
# We intentionally don't add inspect.iscoroutinefunction() check
# for func argument because there is no way
# to check for async function reliably:
# 1. It can be "async def func()" iself
# 2. Class can implement "async def __call__()" method
# 3. Regular "def func()" that returns awaitable object
self.addCleanup(*(func, *args), **kwargs)
def _callSetUp(self):
self.setUp()
self._callAsync(self.asyncSetUp)
def _callTestMethod(self, method):
self._callMaybeAsync(method)
def _callTearDown(self):
self._callAsync(self.asyncTearDown)
self.tearDown()
def _callCleanup(self, function, *args, **kwargs):
self._callMaybeAsync(function, *args, **kwargs)
def _callAsync(self, func, *args, **kwargs):
assert self._asyncioTestLoop is not None
ret = func(*args, **kwargs)
assert inspect.isawaitable(ret)
fut = self._asyncioTestLoop.create_future()
self._asyncioCallsQueue.put_nowait((fut, ret))
return self._asyncioTestLoop.run_until_complete(fut)
def _callMaybeAsync(self, func, *args, **kwargs):
assert self._asyncioTestLoop is not None
ret = func(*args, **kwargs)
if inspect.isawaitable(ret):
fut = self._asyncioTestLoop.create_future()
self._asyncioCallsQueue.put_nowait((fut, ret))
return self._asyncioTestLoop.run_until_complete(fut)
else:
return ret
async def _asyncioLoopRunner(self, fut):
self._asyncioCallsQueue = queue = asyncio.Queue()
fut.set_result(None)
while True:
query = await queue.get()
queue.task_done()
if query is None:
return
fut, awaitable = query
try:
ret = await awaitable
if not fut.cancelled():
fut.set_result(ret)
except asyncio.CancelledError:
raise
except Exception as ex:
if not fut.cancelled():
fut.set_exception(ex)
def _setupAsyncioLoop(self):
assert self._asyncioTestLoop is None
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.set_debug(True)
self._asyncioTestLoop = loop
fut = loop.create_future()
self._asyncioCallsTask = loop.create_task(self._asyncioLoopRunner(fut))
loop.run_until_complete(fut)
def _tearDownAsyncioLoop(self):
assert self._asyncioTestLoop is not None
loop = self._asyncioTestLoop
self._asyncioTestLoop = None
self._asyncioCallsQueue.put_nowait(None)
loop.run_until_complete(self._asyncioCallsQueue.join())
try:
# cancel all tasks
to_cancel = all_tasks(loop)
if not to_cancel:
return
for task in to_cancel:
task.cancel()
loop.run_until_complete(
asyncio.gather(*to_cancel, loop=loop, return_exceptions=True)
)
for task in to_cancel:
if task.cancelled():
continue
if task.exception() is not None:
loop.call_exception_handler(
{
"message": "unhandled exception during test shutdown",
"exception": task.exception(),
"task": task,
}
)
# shutdown asyncgens
loop.run_until_complete(loop.shutdown_asyncgens())
finally:
asyncio.set_event_loop(None)
loop.close()
def run(self, result=None):
self._setupAsyncioLoop()
try:
return super().run(result)
finally:
self._tearDownAsyncioLoop()
python-hl7-0.4.2/tests/backports/unittest/case.py 0000664 0000000 0000000 00000165171 14012562772 0022045 0 ustar 00root root 0000000 0000000 # flake8: noqa
# TODO Remove once 3.7 support is dropped
# .async_case.IsolatedAsyncioTestCase required Python 3.8 TestCase functionality
# From https://github.com/python/cpython/blob/3.8/Lib/unittest/case.py
import collections
import contextlib
import difflib
import functools
import logging
import pprint
import re
import sys
import traceback
import types
import warnings
from unittest import result
from unittest.util import (
_common_shorten_repr,
_count_diff_all_purpose,
_count_diff_hashable,
safe_repr,
strclass,
)
__unittest = True
_subtest_msg_sentinel = object()
DIFF_OMITTED = "\nDiff is %s characters long. " "Set self.maxDiff to None to see it."
class SkipTest(Exception):
"""
Raise this exception in a test to skip it.
Usually you can use TestCase.skipTest() or one of the skipping decorators
instead of raising this directly.
"""
class _ShouldStop(Exception):
"""
The test should stop.
"""
class _UnexpectedSuccess(Exception):
"""
The test was supposed to fail, but it didn't!
"""
class _Outcome(object):
def __init__(self, result=None):
self.expecting_failure = False
self.result = result
self.result_supports_subtests = hasattr(result, "addSubTest")
self.success = True
self.skipped = []
self.expectedFailure = None
self.errors = []
@contextlib.contextmanager
def testPartExecutor(self, test_case, isTest=False):
old_success = self.success
self.success = True
try:
yield
except KeyboardInterrupt:
raise
except SkipTest as e:
self.success = False
self.skipped.append((test_case, str(e)))
except _ShouldStop:
pass
except Exception:
exc_info = sys.exc_info()
if self.expecting_failure:
self.expectedFailure = exc_info
else:
self.success = False
self.errors.append((test_case, exc_info))
# explicitly break a reference cycle:
# exc_info -> frame -> exc_info
exc_info = None
else:
if self.result_supports_subtests and self.success:
self.errors.append((test_case, None))
finally:
self.success = self.success and old_success
def _id(obj):
return obj
_module_cleanups = []
def addModuleCleanup(function, *args, **kwargs):
"""Same as addCleanup, except the cleanup items are called even if
setUpModule fails (unlike tearDownModule)."""
_module_cleanups.append((function, args, kwargs))
def doModuleCleanups():
"""Execute all module cleanup functions. Normally called for you after
tearDownModule."""
exceptions = []
while _module_cleanups:
function, args, kwargs = _module_cleanups.pop()
try:
function(*args, **kwargs)
except Exception as exc:
exceptions.append(exc)
if exceptions:
# Swallows all but first exception. If a multi-exception handler
# gets written we should use that here instead.
raise exceptions[0]
def skip(reason):
"""
Unconditionally skip a test.
"""
def decorator(test_item):
if not isinstance(test_item, type):
@functools.wraps(test_item)
def skip_wrapper(*args, **kwargs):
raise SkipTest(reason)
test_item = skip_wrapper
test_item.__unittest_skip__ = True
test_item.__unittest_skip_why__ = reason
return test_item
if isinstance(reason, types.FunctionType):
test_item = reason
reason = ""
return decorator(test_item)
return decorator
def skipIf(condition, reason):
"""
Skip a test if the condition is true.
"""
if condition:
return skip(reason)
return _id
def skipUnless(condition, reason):
"""
Skip a test unless the condition is true.
"""
if not condition:
return skip(reason)
return _id
def expectedFailure(test_item):
test_item.__unittest_expecting_failure__ = True
return test_item
def _is_subtype(expected, basetype):
if isinstance(expected, tuple):
return all(_is_subtype(e, basetype) for e in expected)
return isinstance(expected, type) and issubclass(expected, basetype)
class _BaseTestCaseContext:
def __init__(self, test_case):
self.test_case = test_case
def _raiseFailure(self, standardMsg):
msg = self.test_case._formatMessage(self.msg, standardMsg)
raise self.test_case.failureException(msg)
class _AssertRaisesBaseContext(_BaseTestCaseContext):
def __init__(self, expected, test_case, expected_regex=None):
_BaseTestCaseContext.__init__(self, test_case)
self.expected = expected
self.test_case = test_case
if expected_regex is not None:
expected_regex = re.compile(expected_regex)
self.expected_regex = expected_regex
self.obj_name = None
self.msg = None
def handle(self, name, args, kwargs):
"""
If args is empty, assertRaises/Warns is being used as a
context manager, so check for a 'msg' kwarg and return self.
If args is not empty, call a callable passing positional and keyword
arguments.
"""
try:
if not _is_subtype(self.expected, self._base_type):
raise TypeError("%s() arg 1 must be %s" % (name, self._base_type_str))
if not args:
self.msg = kwargs.pop("msg", None)
if kwargs:
raise TypeError(
"%r is an invalid keyword argument for "
"this function" % (next(iter(kwargs)),)
)
return self
callable_obj, *args = args
try:
self.obj_name = callable_obj.__name__
except AttributeError:
self.obj_name = str(callable_obj)
with self:
callable_obj(*args, **kwargs)
finally:
# bpo-23890: manually break a reference cycle
self = None
class _AssertRaisesContext(_AssertRaisesBaseContext):
"""A context manager used to implement TestCase.assertRaises* methods."""
_base_type = BaseException
_base_type_str = "an exception type or tuple of exception types"
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, tb):
if exc_type is None:
try:
exc_name = self.expected.__name__
except AttributeError:
exc_name = str(self.expected)
if self.obj_name:
self._raiseFailure(
"{} not raised by {}".format(exc_name, self.obj_name)
)
else:
self._raiseFailure("{} not raised".format(exc_name))
else:
traceback.clear_frames(tb)
if not issubclass(exc_type, self.expected):
# let unexpected exceptions pass through
return False
# store exception, without traceback, for later retrieval
self.exception = exc_value.with_traceback(None)
if self.expected_regex is None:
return True
expected_regex = self.expected_regex
if not expected_regex.search(str(exc_value)):
self._raiseFailure(
'"{}" does not match "{}"'.format(
expected_regex.pattern, str(exc_value)
)
)
return True
class _AssertWarnsContext(_AssertRaisesBaseContext):
"""A context manager used to implement TestCase.assertWarns* methods."""
_base_type = Warning
_base_type_str = "a warning type or tuple of warning types"
def __enter__(self):
# The __warningregistry__'s need to be in a pristine state for tests
# to work properly.
for v in list(sys.modules.values()):
if getattr(v, "__warningregistry__", None):
v.__warningregistry__ = {}
self.warnings_manager = warnings.catch_warnings(record=True)
self.warnings = self.warnings_manager.__enter__()
warnings.simplefilter("always", self.expected)
return self
def __exit__(self, exc_type, exc_value, tb):
self.warnings_manager.__exit__(exc_type, exc_value, tb)
if exc_type is not None:
# let unexpected exceptions pass through
return
try:
exc_name = self.expected.__name__
except AttributeError:
exc_name = str(self.expected)
first_matching = None
for m in self.warnings:
w = m.message
if not isinstance(w, self.expected):
continue
if first_matching is None:
first_matching = w
if self.expected_regex is not None and not self.expected_regex.search(
str(w)
):
continue
# store warning for later retrieval
self.warning = w
self.filename = m.filename
self.lineno = m.lineno
return
# Now we simply try to choose a helpful failure message
if first_matching is not None:
self._raiseFailure(
'"{}" does not match "{}"'.format(
self.expected_regex.pattern, str(first_matching)
)
)
if self.obj_name:
self._raiseFailure("{} not triggered by {}".format(exc_name, self.obj_name))
else:
self._raiseFailure("{} not triggered".format(exc_name))
_LoggingWatcher = collections.namedtuple("_LoggingWatcher", ["records", "output"])
class _CapturingHandler(logging.Handler):
"""
A logging handler capturing all (raw and formatted) logging output.
"""
def __init__(self):
logging.Handler.__init__(self)
self.watcher = _LoggingWatcher([], [])
def flush(self):
pass
def emit(self, record):
self.watcher.records.append(record)
msg = self.format(record)
self.watcher.output.append(msg)
class _AssertLogsContext(_BaseTestCaseContext):
"""A context manager used to implement TestCase.assertLogs()."""
LOGGING_FORMAT = "%(levelname)s:%(name)s:%(message)s"
def __init__(self, test_case, logger_name, level):
_BaseTestCaseContext.__init__(self, test_case)
self.logger_name = logger_name
if level:
self.level = logging._nameToLevel.get(level, level)
else:
self.level = logging.INFO
self.msg = None
def __enter__(self):
if isinstance(self.logger_name, logging.Logger):
logger = self.logger = self.logger_name
else:
logger = self.logger = logging.getLogger(self.logger_name)
formatter = logging.Formatter(self.LOGGING_FORMAT)
handler = _CapturingHandler()
handler.setFormatter(formatter)
self.watcher = handler.watcher
self.old_handlers = logger.handlers[:]
self.old_level = logger.level
self.old_propagate = logger.propagate
logger.handlers = [handler]
logger.setLevel(self.level)
logger.propagate = False
return handler.watcher
def __exit__(self, exc_type, exc_value, tb):
self.logger.handlers = self.old_handlers
self.logger.propagate = self.old_propagate
self.logger.setLevel(self.old_level)
if exc_type is not None:
# let unexpected exceptions pass through
return False
if len(self.watcher.records) == 0:
self._raiseFailure(
"no logs of level {} or higher triggered on {}".format(
logging.getLevelName(self.level), self.logger.name
)
)
class _OrderedChainMap(collections.ChainMap):
def __iter__(self):
seen = set()
for mapping in self.maps:
for k in mapping:
if k not in seen:
seen.add(k)
yield k
class TestCase(object):
"""A class whose instances are single test cases.
By default, the test code itself should be placed in a method named
'runTest'.
If the fixture may be used for many test cases, create as
many test methods as are needed. When instantiating such a TestCase
subclass, specify in the constructor arguments the name of the test method
that the instance is to execute.
Test authors should subclass TestCase for their own tests. Construction
and deconstruction of the test's environment ('fixture') can be
implemented by overriding the 'setUp' and 'tearDown' methods respectively.
If it is necessary to override the __init__ method, the base class
__init__ method must always be called. It is important that subclasses
should not change the signature of their __init__ method, since instances
of the classes are instantiated automatically by parts of the framework
in order to be run.
When subclassing TestCase, you can set these attributes:
* failureException: determines which exception will be raised when
the instance's assertion methods fail; test methods raising this
exception will be deemed to have 'failed' rather than 'errored'.
* longMessage: determines whether long messages (including repr of
objects used in assert methods) will be printed on failure in *addition*
to any explicit message passed.
* maxDiff: sets the maximum length of a diff in failure messages
by assert methods using difflib. It is looked up as an instance
attribute so can be configured by individual tests if required.
"""
failureException = AssertionError
longMessage = True
maxDiff = 80 * 8
# If a string is longer than _diffThreshold, use normal comparison instead
# of difflib. See #11763.
_diffThreshold = 2 ** 16
# Attribute used by TestSuite for classSetUp
_classSetupFailed = False
_class_cleanups = []
def __init__(self, methodName="runTest"):
"""Create an instance of the class that will use the named test
method when executed. Raises a ValueError if the instance does
not have a method with the specified name.
"""
self._testMethodName = methodName
self._outcome = None
self._testMethodDoc = "No test"
try:
testMethod = getattr(self, methodName)
except AttributeError:
if methodName != "runTest":
# we allow instantiation with no explicit method name
# but not an *incorrect* or missing method name
raise ValueError(
"no such test method in %s: %s" % (self.__class__, methodName)
)
else:
self._testMethodDoc = testMethod.__doc__
self._cleanups = []
self._subtest = None
# Map types to custom assertEqual functions that will compare
# instances of said type in more detail to generate a more useful
# error message.
self._type_equality_funcs = {}
self.addTypeEqualityFunc(dict, "assertDictEqual")
self.addTypeEqualityFunc(list, "assertListEqual")
self.addTypeEqualityFunc(tuple, "assertTupleEqual")
self.addTypeEqualityFunc(set, "assertSetEqual")
self.addTypeEqualityFunc(frozenset, "assertSetEqual")
self.addTypeEqualityFunc(str, "assertMultiLineEqual")
def addTypeEqualityFunc(self, typeobj, function):
"""Add a type specific assertEqual style function to compare a type.
This method is for use by TestCase subclasses that need to register
their own type equality functions to provide nicer error messages.
Args:
typeobj: The data type to call this function on when both values
are of the same type in assertEqual().
function: The callable taking two arguments and an optional
msg= argument that raises self.failureException with a
useful error message when the two arguments are not equal.
"""
self._type_equality_funcs[typeobj] = function
def addCleanup(*args, **kwargs):
"""Add a function, with arguments, to be called when the test is
completed. Functions added are called on a LIFO basis and are
called after tearDown on test failure or success.
Cleanup items are called even if setUp fails (unlike tearDown)."""
if len(args) >= 2:
self, function, *args = args
elif not args:
raise TypeError(
"descriptor 'addCleanup' of 'TestCase' object " "needs an argument"
)
elif "function" in kwargs:
function = kwargs.pop("function")
self, *args = args
import warnings
warnings.warn(
"Passing 'function' as keyword argument is deprecated",
DeprecationWarning,
stacklevel=2,
)
else:
raise TypeError(
"addCleanup expected at least 1 positional "
"argument, got %d" % (len(args) - 1)
)
args = tuple(args)
self._cleanups.append((function, args, kwargs))
addCleanup.__text_signature__ = "($self, function, /, *args, **kwargs)"
@classmethod
def addClassCleanup(cls, function, *args, **kwargs):
"""Same as addCleanup, except the cleanup items are called even if
setUpClass fails (unlike tearDownClass)."""
cls._class_cleanups.append((function, args, kwargs))
def setUp(self):
"Hook method for setting up the test fixture before exercising it."
pass
def tearDown(self):
"Hook method for deconstructing the test fixture after testing it."
pass
@classmethod
def setUpClass(cls):
"Hook method for setting up class fixture before running tests in the class."
@classmethod
def tearDownClass(cls):
"Hook method for deconstructing the class fixture after running all tests in the class."
def countTestCases(self):
return 1
def defaultTestResult(self):
return result.TestResult()
def shortDescription(self):
"""Returns a one-line description of the test, or None if no
description has been provided.
The default implementation of this method returns the first line of
the specified test method's docstring.
"""
doc = self._testMethodDoc
return doc.strip().split("\n")[0].strip() if doc else None
def id(self):
return "%s.%s" % (strclass(self.__class__), self._testMethodName)
def __eq__(self, other):
if type(self) is not type(other):
return NotImplemented
return self._testMethodName == other._testMethodName
def __hash__(self):
return hash((type(self), self._testMethodName))
def __str__(self):
return "%s (%s)" % (self._testMethodName, strclass(self.__class__))
def __repr__(self):
return "<%s testMethod=%s>" % (strclass(self.__class__), self._testMethodName)
def _addSkip(self, result, test_case, reason):
addSkip = getattr(result, "addSkip", None)
if addSkip is not None:
addSkip(test_case, reason)
else:
warnings.warn(
"TestResult has no addSkip method, skips not reported",
RuntimeWarning,
2,
)
result.addSuccess(test_case)
@contextlib.contextmanager
def subTest(self, msg=_subtest_msg_sentinel, **params):
"""Return a context manager that will return the enclosed block
of code in a subtest identified by the optional message and
keyword parameters. A failure in the subtest marks the test
case as failed but resumes execution at the end of the enclosed
block, allowing further test code to be executed.
"""
if self._outcome is None or not self._outcome.result_supports_subtests:
yield
return
parent = self._subtest
if parent is None:
params_map = _OrderedChainMap(params)
else:
params_map = parent.params.new_child(params)
self._subtest = _SubTest(self, msg, params_map)
try:
with self._outcome.testPartExecutor(self._subtest, isTest=True):
yield
if not self._outcome.success:
result = self._outcome.result
if result is not None and result.failfast:
raise _ShouldStop
elif self._outcome.expectedFailure:
# If the test is expecting a failure, we really want to
# stop now and register the expected failure.
raise _ShouldStop
finally:
self._subtest = parent
def _feedErrorsToResult(self, result, errors):
for test, exc_info in errors:
if isinstance(test, _SubTest):
result.addSubTest(test.test_case, test, exc_info)
elif exc_info is not None:
if issubclass(exc_info[0], self.failureException):
result.addFailure(test, exc_info)
else:
result.addError(test, exc_info)
def _addExpectedFailure(self, result, exc_info):
try:
addExpectedFailure = result.addExpectedFailure
except AttributeError:
warnings.warn(
"TestResult has no addExpectedFailure method, reporting as passes",
RuntimeWarning,
)
result.addSuccess(self)
else:
addExpectedFailure(self, exc_info)
def _addUnexpectedSuccess(self, result):
try:
addUnexpectedSuccess = result.addUnexpectedSuccess
except AttributeError:
warnings.warn(
"TestResult has no addUnexpectedSuccess method, reporting as failure",
RuntimeWarning,
)
# We need to pass an actual exception and traceback to addFailure,
# otherwise the legacy result can choke.
try:
raise _UnexpectedSuccess from None
except _UnexpectedSuccess:
result.addFailure(self, sys.exc_info())
else:
addUnexpectedSuccess(self)
def _callSetUp(self):
self.setUp()
def _callTestMethod(self, method):
method()
def _callTearDown(self):
self.tearDown()
def _callCleanup(self, function, *args, **kwargs):
function(*args, **kwargs)
def run(self, result=None):
orig_result = result
if result is None:
result = self.defaultTestResult()
startTestRun = getattr(result, "startTestRun", None)
if startTestRun is not None:
startTestRun()
result.startTest(self)
testMethod = getattr(self, self._testMethodName)
if getattr(self.__class__, "__unittest_skip__", False) or getattr(
testMethod, "__unittest_skip__", False
):
# If the class or method was skipped.
try:
skip_why = getattr(
self.__class__, "__unittest_skip_why__", ""
) or getattr(testMethod, "__unittest_skip_why__", "")
self._addSkip(result, self, skip_why)
finally:
result.stopTest(self)
return
expecting_failure_method = getattr(
testMethod, "__unittest_expecting_failure__", False
)
expecting_failure_class = getattr(self, "__unittest_expecting_failure__", False)
expecting_failure = expecting_failure_class or expecting_failure_method
outcome = _Outcome(result)
try:
self._outcome = outcome
with outcome.testPartExecutor(self):
self._callSetUp()
if outcome.success:
outcome.expecting_failure = expecting_failure
with outcome.testPartExecutor(self, isTest=True):
self._callTestMethod(testMethod)
outcome.expecting_failure = False
with outcome.testPartExecutor(self):
self._callTearDown()
self.doCleanups()
for test, reason in outcome.skipped:
self._addSkip(result, test, reason)
self._feedErrorsToResult(result, outcome.errors)
if outcome.success:
if expecting_failure:
if outcome.expectedFailure:
self._addExpectedFailure(result, outcome.expectedFailure)
else:
self._addUnexpectedSuccess(result)
else:
result.addSuccess(self)
return result
finally:
result.stopTest(self)
if orig_result is None:
stopTestRun = getattr(result, "stopTestRun", None)
if stopTestRun is not None:
stopTestRun()
# explicitly break reference cycles:
# outcome.errors -> frame -> outcome -> outcome.errors
# outcome.expectedFailure -> frame -> outcome -> outcome.expectedFailure
outcome.errors.clear()
outcome.expectedFailure = None
# clear the outcome, no more needed
self._outcome = None
def doCleanups(self):
"""Execute all cleanup functions. Normally called for you after
tearDown."""
outcome = self._outcome or _Outcome()
while self._cleanups:
function, args, kwargs = self._cleanups.pop()
with outcome.testPartExecutor(self):
self._callCleanup(function, *args, **kwargs)
# return this for backwards compatibility
# even though we no longer use it internally
return outcome.success
@classmethod
def doClassCleanups(cls):
"""Execute all class cleanup functions. Normally called for you after
tearDownClass."""
cls.tearDown_exceptions = []
while cls._class_cleanups:
function, args, kwargs = cls._class_cleanups.pop()
try:
function(*args, **kwargs)
except Exception:
cls.tearDown_exceptions.append(sys.exc_info())
def __call__(self, *args, **kwds):
return self.run(*args, **kwds)
def debug(self):
"""Run the test without collecting errors in a TestResult"""
self.setUp()
getattr(self, self._testMethodName)()
self.tearDown()
while self._cleanups:
function, args, kwargs = self._cleanups.pop(-1)
function(*args, **kwargs)
def skipTest(self, reason):
"""Skip this test."""
raise SkipTest(reason)
def fail(self, msg=None):
"""Fail immediately, with the given message."""
raise self.failureException(msg)
def assertFalse(self, expr, msg=None):
"""Check that the expression is false."""
if expr:
msg = self._formatMessage(msg, "%s is not false" % safe_repr(expr))
raise self.failureException(msg)
def assertTrue(self, expr, msg=None):
"""Check that the expression is true."""
if not expr:
msg = self._formatMessage(msg, "%s is not true" % safe_repr(expr))
raise self.failureException(msg)
def _formatMessage(self, msg, standardMsg):
"""Honour the longMessage attribute when generating failure messages.
If longMessage is False this means:
* Use only an explicit message if it is provided
* Otherwise use the standard message for the assert
If longMessage is True:
* Use the standard message
* If an explicit message is provided, plus ' : ' and the explicit message
"""
if not self.longMessage:
return msg or standardMsg
if msg is None:
return standardMsg
try:
# don't switch to '{}' formatting in Python 2.X
# it changes the way unicode input is handled
return "%s : %s" % (standardMsg, msg)
except UnicodeDecodeError:
return "%s : %s" % (safe_repr(standardMsg), safe_repr(msg))
def assertRaises(self, expected_exception, *args, **kwargs):
"""Fail unless an exception of class expected_exception is raised
by the callable when invoked with specified positional and
keyword arguments. If a different type of exception is
raised, it will not be caught, and the test case will be
deemed to have suffered an error, exactly as for an
unexpected exception.
If called with the callable and arguments omitted, will return a
context object used like this::
with self.assertRaises(SomeException):
do_something()
An optional keyword argument 'msg' can be provided when assertRaises
is used as a context object.
The context manager keeps a reference to the exception as
the 'exception' attribute. This allows you to inspect the
exception after the assertion::
with self.assertRaises(SomeException) as cm:
do_something()
the_exception = cm.exception
self.assertEqual(the_exception.error_code, 3)
"""
context = _AssertRaisesContext(expected_exception, self)
try:
return context.handle("assertRaises", args, kwargs)
finally:
# bpo-23890: manually break a reference cycle
context = None
def assertWarns(self, expected_warning, *args, **kwargs):
"""Fail unless a warning of class warnClass is triggered
by the callable when invoked with specified positional and
keyword arguments. If a different type of warning is
triggered, it will not be handled: depending on the other
warning filtering rules in effect, it might be silenced, printed
out, or raised as an exception.
If called with the callable and arguments omitted, will return a
context object used like this::
with self.assertWarns(SomeWarning):
do_something()
An optional keyword argument 'msg' can be provided when assertWarns
is used as a context object.
The context manager keeps a reference to the first matching
warning as the 'warning' attribute; similarly, the 'filename'
and 'lineno' attributes give you information about the line
of Python code from which the warning was triggered.
This allows you to inspect the warning after the assertion::
with self.assertWarns(SomeWarning) as cm:
do_something()
the_warning = cm.warning
self.assertEqual(the_warning.some_attribute, 147)
"""
context = _AssertWarnsContext(expected_warning, self)
return context.handle("assertWarns", args, kwargs)
def assertLogs(self, logger=None, level=None):
"""Fail unless a log message of level *level* or higher is emitted
on *logger_name* or its children. If omitted, *level* defaults to
INFO and *logger* defaults to the root logger.
This method must be used as a context manager, and will yield
a recording object with two attributes: `output` and `records`.
At the end of the context manager, the `output` attribute will
be a list of the matching formatted log messages and the
`records` attribute will be a list of the corresponding LogRecord
objects.
Example::
with self.assertLogs('foo', level='INFO') as cm:
logging.getLogger('foo').info('first message')
logging.getLogger('foo.bar').error('second message')
self.assertEqual(cm.output, ['INFO:foo:first message',
'ERROR:foo.bar:second message'])
"""
return _AssertLogsContext(self, logger, level)
def _getAssertEqualityFunc(self, first, second):
"""Get a detailed comparison function for the types of the two args.
Returns: A callable accepting (first, second, msg=None) that will
raise a failure exception if first != second with a useful human
readable error message for those types.
"""
#
# NOTE(gregory.p.smith): I considered isinstance(first, type(second))
# and vice versa. I opted for the conservative approach in case
# subclasses are not intended to be compared in detail to their super
# class instances using a type equality func. This means testing
# subtypes won't automagically use the detailed comparison. Callers
# should use their type specific assertSpamEqual method to compare
# subclasses if the detailed comparison is desired and appropriate.
# See the discussion in http://bugs.python.org/issue2578.
#
if type(first) is type(second):
asserter = self._type_equality_funcs.get(type(first))
if asserter is not None:
if isinstance(asserter, str):
asserter = getattr(self, asserter)
return asserter
return self._baseAssertEqual
def _baseAssertEqual(self, first, second, msg=None):
"""The default assertEqual implementation, not type specific."""
if not first == second:
standardMsg = "%s != %s" % _common_shorten_repr(first, second)
msg = self._formatMessage(msg, standardMsg)
raise self.failureException(msg)
def assertEqual(self, first, second, msg=None):
"""Fail if the two objects are unequal as determined by the '=='
operator.
"""
assertion_func = self._getAssertEqualityFunc(first, second)
assertion_func(first, second, msg=msg)
def assertNotEqual(self, first, second, msg=None):
"""Fail if the two objects are equal as determined by the '!='
operator.
"""
if not first != second:
msg = self._formatMessage(
msg, "%s == %s" % (safe_repr(first), safe_repr(second))
)
raise self.failureException(msg)
def assertAlmostEqual(self, first, second, places=None, msg=None, delta=None):
"""Fail if the two objects are unequal as determined by their
difference rounded to the given number of decimal places
(default 7) and comparing to zero, or by comparing that the
difference between the two objects is more than the given
delta.
Note that decimal places (from zero) are usually not the same
as significant digits (measured from the most significant digit).
If the two objects compare equal then they will automatically
compare almost equal.
"""
if first == second:
# shortcut
return
if delta is not None and places is not None:
raise TypeError("specify delta or places not both")
diff = abs(first - second)
if delta is not None:
if diff <= delta:
return
standardMsg = "%s != %s within %s delta (%s difference)" % (
safe_repr(first),
safe_repr(second),
safe_repr(delta),
safe_repr(diff),
)
else:
if places is None:
places = 7
if round(diff, places) == 0:
return
standardMsg = "%s != %s within %r places (%s difference)" % (
safe_repr(first),
safe_repr(second),
places,
safe_repr(diff),
)
msg = self._formatMessage(msg, standardMsg)
raise self.failureException(msg)
def assertNotAlmostEqual(self, first, second, places=None, msg=None, delta=None):
"""Fail if the two objects are equal as determined by their
difference rounded to the given number of decimal places
(default 7) and comparing to zero, or by comparing that the
difference between the two objects is less than the given delta.
Note that decimal places (from zero) are usually not the same
as significant digits (measured from the most significant digit).
Objects that are equal automatically fail.
"""
if delta is not None and places is not None:
raise TypeError("specify delta or places not both")
diff = abs(first - second)
if delta is not None:
if not (first == second) and diff > delta:
return
standardMsg = "%s == %s within %s delta (%s difference)" % (
safe_repr(first),
safe_repr(second),
safe_repr(delta),
safe_repr(diff),
)
else:
if places is None:
places = 7
if not (first == second) and round(diff, places) != 0:
return
standardMsg = "%s == %s within %r places" % (
safe_repr(first),
safe_repr(second),
places,
)
msg = self._formatMessage(msg, standardMsg)
raise self.failureException(msg)
def assertSequenceEqual(self, seq1, seq2, msg=None, seq_type=None):
"""An equality assertion for ordered sequences (like lists and tuples).
For the purposes of this function, a valid ordered sequence type is one
which can be indexed, has a length, and has an equality operator.
Args:
seq1: The first sequence to compare.
seq2: The second sequence to compare.
seq_type: The expected datatype of the sequences, or None if no
datatype should be enforced.
msg: Optional message to use on failure instead of a list of
differences.
"""
if seq_type is not None:
seq_type_name = seq_type.__name__
if not isinstance(seq1, seq_type):
raise self.failureException(
"First sequence is not a %s: %s" % (seq_type_name, safe_repr(seq1))
)
if not isinstance(seq2, seq_type):
raise self.failureException(
"Second sequence is not a %s: %s" % (seq_type_name, safe_repr(seq2))
)
else:
seq_type_name = "sequence"
differing = None
try:
len1 = len(seq1)
except (TypeError, NotImplementedError):
differing = "First %s has no length. Non-sequence?" % (seq_type_name)
if differing is None:
try:
len2 = len(seq2)
except (TypeError, NotImplementedError):
differing = "Second %s has no length. Non-sequence?" % (
seq_type_name
)
if differing is None:
if seq1 == seq2:
return
differing = "%ss differ: %s != %s\n" % (
(seq_type_name.capitalize(),) + _common_shorten_repr(seq1, seq2)
)
for i in range(min(len1, len2)):
try:
item1 = seq1[i]
except (TypeError, IndexError, NotImplementedError):
differing += "\nUnable to index element %d of first %s\n" % (
i,
seq_type_name,
)
break
try:
item2 = seq2[i]
except (TypeError, IndexError, NotImplementedError):
differing += "\nUnable to index element %d of second %s\n" % (
i,
seq_type_name,
)
break
if item1 != item2:
differing += "\nFirst differing element %d:\n%s\n%s\n" % (
(i,) + _common_shorten_repr(item1, item2)
)
break
else:
if len1 == len2 and seq_type is None and type(seq1) != type(seq2):
# The sequences are the same, but have differing types.
return
if len1 > len2:
differing += "\nFirst %s contains %d additional " "elements.\n" % (
seq_type_name,
len1 - len2,
)
try:
differing += "First extra element %d:\n%s\n" % (
len2,
safe_repr(seq1[len2]),
)
except (TypeError, IndexError, NotImplementedError):
differing += "Unable to index element %d " "of first %s\n" % (
len2,
seq_type_name,
)
elif len1 < len2:
differing += "\nSecond %s contains %d additional " "elements.\n" % (
seq_type_name,
len2 - len1,
)
try:
differing += "First extra element %d:\n%s\n" % (
len1,
safe_repr(seq2[len1]),
)
except (TypeError, IndexError, NotImplementedError):
differing += "Unable to index element %d " "of second %s\n" % (
len1,
seq_type_name,
)
standardMsg = differing
diffMsg = "\n" + "\n".join(
difflib.ndiff(
pprint.pformat(seq1).splitlines(), pprint.pformat(seq2).splitlines()
)
)
standardMsg = self._truncateMessage(standardMsg, diffMsg)
msg = self._formatMessage(msg, standardMsg)
self.fail(msg)
def _truncateMessage(self, message, diff):
max_diff = self.maxDiff
if max_diff is None or len(diff) <= max_diff:
return message + diff
return message + (DIFF_OMITTED % len(diff))
def assertListEqual(self, list1, list2, msg=None):
"""A list-specific equality assertion.
Args:
list1: The first list to compare.
list2: The second list to compare.
msg: Optional message to use on failure instead of a list of
differences.
"""
self.assertSequenceEqual(list1, list2, msg, seq_type=list)
def assertTupleEqual(self, tuple1, tuple2, msg=None):
"""A tuple-specific equality assertion.
Args:
tuple1: The first tuple to compare.
tuple2: The second tuple to compare.
msg: Optional message to use on failure instead of a list of
differences.
"""
self.assertSequenceEqual(tuple1, tuple2, msg, seq_type=tuple)
def assertSetEqual(self, set1, set2, msg=None):
"""A set-specific equality assertion.
Args:
set1: The first set to compare.
set2: The second set to compare.
msg: Optional message to use on failure instead of a list of
differences.
assertSetEqual uses ducktyping to support different types of sets, and
is optimized for sets specifically (parameters must support a
difference method).
"""
try:
difference1 = set1.difference(set2)
except TypeError as e:
self.fail("invalid type when attempting set difference: %s" % e)
except AttributeError as e:
self.fail("first argument does not support set difference: %s" % e)
try:
difference2 = set2.difference(set1)
except TypeError as e:
self.fail("invalid type when attempting set difference: %s" % e)
except AttributeError as e:
self.fail("second argument does not support set difference: %s" % e)
if not (difference1 or difference2):
return
lines = []
if difference1:
lines.append("Items in the first set but not the second:")
for item in difference1:
lines.append(repr(item))
if difference2:
lines.append("Items in the second set but not the first:")
for item in difference2:
lines.append(repr(item))
standardMsg = "\n".join(lines)
self.fail(self._formatMessage(msg, standardMsg))
def assertIn(self, member, container, msg=None):
"""Just like self.assertTrue(a in b), but with a nicer default message."""
if member not in container:
standardMsg = "%s not found in %s" % (
safe_repr(member),
safe_repr(container),
)
self.fail(self._formatMessage(msg, standardMsg))
def assertNotIn(self, member, container, msg=None):
"""Just like self.assertTrue(a not in b), but with a nicer default message."""
if member in container:
standardMsg = "%s unexpectedly found in %s" % (
safe_repr(member),
safe_repr(container),
)
self.fail(self._formatMessage(msg, standardMsg))
def assertIs(self, expr1, expr2, msg=None):
"""Just like self.assertTrue(a is b), but with a nicer default message."""
if expr1 is not expr2:
standardMsg = "%s is not %s" % (safe_repr(expr1), safe_repr(expr2))
self.fail(self._formatMessage(msg, standardMsg))
def assertIsNot(self, expr1, expr2, msg=None):
"""Just like self.assertTrue(a is not b), but with a nicer default message."""
if expr1 is expr2:
standardMsg = "unexpectedly identical: %s" % (safe_repr(expr1),)
self.fail(self._formatMessage(msg, standardMsg))
def assertDictEqual(self, d1, d2, msg=None):
self.assertIsInstance(d1, dict, "First argument is not a dictionary")
self.assertIsInstance(d2, dict, "Second argument is not a dictionary")
if d1 != d2:
standardMsg = "%s != %s" % _common_shorten_repr(d1, d2)
diff = "\n" + "\n".join(
difflib.ndiff(
pprint.pformat(d1).splitlines(), pprint.pformat(d2).splitlines()
)
)
standardMsg = self._truncateMessage(standardMsg, diff)
self.fail(self._formatMessage(msg, standardMsg))
def assertDictContainsSubset(self, subset, dictionary, msg=None):
"""Checks whether dictionary is a superset of subset."""
warnings.warn("assertDictContainsSubset is deprecated", DeprecationWarning)
missing = []
mismatched = []
for key, value in subset.items():
if key not in dictionary:
missing.append(key)
elif value != dictionary[key]:
mismatched.append(
"%s, expected: %s, actual: %s"
% (safe_repr(key), safe_repr(value), safe_repr(dictionary[key]))
)
if not (missing or mismatched):
return
standardMsg = ""
if missing:
standardMsg = "Missing: %s" % ",".join(safe_repr(m) for m in missing)
if mismatched:
if standardMsg:
standardMsg += "; "
standardMsg += "Mismatched values: %s" % ",".join(mismatched)
self.fail(self._formatMessage(msg, standardMsg))
def assertCountEqual(self, first, second, msg=None):
"""Asserts that two iterables have the same elements, the same number of
times, without regard to order.
self.assertEqual(Counter(list(first)),
Counter(list(second)))
Example:
- [0, 1, 1] and [1, 0, 1] compare equal.
- [0, 0, 1] and [0, 1] compare unequal.
"""
first_seq, second_seq = list(first), list(second)
try:
first = collections.Counter(first_seq)
second = collections.Counter(second_seq)
except TypeError:
# Handle case with unhashable elements
differences = _count_diff_all_purpose(first_seq, second_seq)
else:
if first == second:
return
differences = _count_diff_hashable(first_seq, second_seq)
if differences:
standardMsg = "Element counts were not equal:\n"
lines = ["First has %d, Second has %d: %r" % diff for diff in differences]
diffMsg = "\n".join(lines)
standardMsg = self._truncateMessage(standardMsg, diffMsg)
msg = self._formatMessage(msg, standardMsg)
self.fail(msg)
def assertMultiLineEqual(self, first, second, msg=None):
"""Assert that two multi-line strings are equal."""
self.assertIsInstance(first, str, "First argument is not a string")
self.assertIsInstance(second, str, "Second argument is not a string")
if first != second:
# don't use difflib if the strings are too long
if len(first) > self._diffThreshold or len(second) > self._diffThreshold:
self._baseAssertEqual(first, second, msg)
firstlines = first.splitlines(keepends=True)
secondlines = second.splitlines(keepends=True)
if len(firstlines) == 1 and first.strip("\r\n") == first:
firstlines = [first + "\n"]
secondlines = [second + "\n"]
standardMsg = "%s != %s" % _common_shorten_repr(first, second)
diff = "\n" + "".join(difflib.ndiff(firstlines, secondlines))
standardMsg = self._truncateMessage(standardMsg, diff)
self.fail(self._formatMessage(msg, standardMsg))
def assertLess(self, a, b, msg=None):
"""Just like self.assertTrue(a < b), but with a nicer default message."""
if not a < b:
standardMsg = "%s not less than %s" % (safe_repr(a), safe_repr(b))
self.fail(self._formatMessage(msg, standardMsg))
def assertLessEqual(self, a, b, msg=None):
"""Just like self.assertTrue(a <= b), but with a nicer default message."""
if not a <= b:
standardMsg = "%s not less than or equal to %s" % (
safe_repr(a),
safe_repr(b),
)
self.fail(self._formatMessage(msg, standardMsg))
def assertGreater(self, a, b, msg=None):
"""Just like self.assertTrue(a > b), but with a nicer default message."""
if not a > b:
standardMsg = "%s not greater than %s" % (safe_repr(a), safe_repr(b))
self.fail(self._formatMessage(msg, standardMsg))
def assertGreaterEqual(self, a, b, msg=None):
"""Just like self.assertTrue(a >= b), but with a nicer default message."""
if not a >= b:
standardMsg = "%s not greater than or equal to %s" % (
safe_repr(a),
safe_repr(b),
)
self.fail(self._formatMessage(msg, standardMsg))
def assertIsNone(self, obj, msg=None):
"""Same as self.assertTrue(obj is None), with a nicer default message."""
if obj is not None:
standardMsg = "%s is not None" % (safe_repr(obj),)
self.fail(self._formatMessage(msg, standardMsg))
def assertIsNotNone(self, obj, msg=None):
"""Included for symmetry with assertIsNone."""
if obj is None:
standardMsg = "unexpectedly None"
self.fail(self._formatMessage(msg, standardMsg))
def assertIsInstance(self, obj, cls, msg=None):
"""Same as self.assertTrue(isinstance(obj, cls)), with a nicer
default message."""
if not isinstance(obj, cls):
standardMsg = "%s is not an instance of %r" % (safe_repr(obj), cls)
self.fail(self._formatMessage(msg, standardMsg))
def assertNotIsInstance(self, obj, cls, msg=None):
"""Included for symmetry with assertIsInstance."""
if isinstance(obj, cls):
standardMsg = "%s is an instance of %r" % (safe_repr(obj), cls)
self.fail(self._formatMessage(msg, standardMsg))
def assertRaisesRegex(self, expected_exception, expected_regex, *args, **kwargs):
"""Asserts that the message in a raised exception matches a regex.
Args:
expected_exception: Exception class expected to be raised.
expected_regex: Regex (re.Pattern object or string) expected
to be found in error message.
args: Function to be called and extra positional args.
kwargs: Extra kwargs.
msg: Optional message used in case of failure. Can only be used
when assertRaisesRegex is used as a context manager.
"""
context = _AssertRaisesContext(expected_exception, self, expected_regex)
return context.handle("assertRaisesRegex", args, kwargs)
def assertWarnsRegex(self, expected_warning, expected_regex, *args, **kwargs):
"""Asserts that the message in a triggered warning matches a regexp.
Basic functioning is similar to assertWarns() with the addition
that only warnings whose messages also match the regular expression
are considered successful matches.
Args:
expected_warning: Warning class expected to be triggered.
expected_regex: Regex (re.Pattern object or string) expected
to be found in error message.
args: Function to be called and extra positional args.
kwargs: Extra kwargs.
msg: Optional message used in case of failure. Can only be used
when assertWarnsRegex is used as a context manager.
"""
context = _AssertWarnsContext(expected_warning, self, expected_regex)
return context.handle("assertWarnsRegex", args, kwargs)
def assertRegex(self, text, expected_regex, msg=None):
"""Fail the test unless the text matches the regular expression."""
if isinstance(expected_regex, (str, bytes)):
assert expected_regex, "expected_regex must not be empty."
expected_regex = re.compile(expected_regex)
if not expected_regex.search(text):
standardMsg = "Regex didn't match: %r not found in %r" % (
expected_regex.pattern,
text,
)
# _formatMessage ensures the longMessage option is respected
msg = self._formatMessage(msg, standardMsg)
raise self.failureException(msg)
def assertNotRegex(self, text, unexpected_regex, msg=None):
"""Fail the test if the text matches the regular expression."""
if isinstance(unexpected_regex, (str, bytes)):
unexpected_regex = re.compile(unexpected_regex)
match = unexpected_regex.search(text)
if match:
standardMsg = "Regex matched: %r matches %r in %r" % (
text[match.start() : match.end()],
unexpected_regex.pattern,
text,
)
# _formatMessage ensures the longMessage option is respected
msg = self._formatMessage(msg, standardMsg)
raise self.failureException(msg)
def _deprecate(original_func):
def deprecated_func(*args, **kwargs):
warnings.warn(
"Please use {0} instead.".format(original_func.__name__),
DeprecationWarning,
2,
)
return original_func(*args, **kwargs)
return deprecated_func
# see #9424
failUnlessEqual = assertEquals = _deprecate(assertEqual)
failIfEqual = assertNotEquals = _deprecate(assertNotEqual)
failUnlessAlmostEqual = assertAlmostEquals = _deprecate(assertAlmostEqual)
failIfAlmostEqual = assertNotAlmostEquals = _deprecate(assertNotAlmostEqual)
failUnless = assert_ = _deprecate(assertTrue)
failUnlessRaises = _deprecate(assertRaises)
failIf = _deprecate(assertFalse)
assertRaisesRegexp = _deprecate(assertRaisesRegex)
assertRegexpMatches = _deprecate(assertRegex)
assertNotRegexpMatches = _deprecate(assertNotRegex)
class FunctionTestCase(TestCase):
"""A test case that wraps a test function.
This is useful for slipping pre-existing test functions into the
unittest framework. Optionally, set-up and tidy-up functions can be
supplied. As with TestCase, the tidy-up ('tearDown') function will
always be called if the set-up ('setUp') function ran successfully.
"""
def __init__(self, testFunc, setUp=None, tearDown=None, description=None):
super(FunctionTestCase, self).__init__()
self._setUpFunc = setUp
self._tearDownFunc = tearDown
self._testFunc = testFunc
self._description = description
def setUp(self):
if self._setUpFunc is not None:
self._setUpFunc()
def tearDown(self):
if self._tearDownFunc is not None:
self._tearDownFunc()
def runTest(self):
self._testFunc()
def id(self):
return self._testFunc.__name__
def __eq__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return (
self._setUpFunc == other._setUpFunc
and self._tearDownFunc == other._tearDownFunc
and self._testFunc == other._testFunc
and self._description == other._description
)
def __hash__(self):
return hash(
(
type(self),
self._setUpFunc,
self._tearDownFunc,
self._testFunc,
self._description,
)
)
def __str__(self):
return "%s (%s)" % (strclass(self.__class__), self._testFunc.__name__)
def __repr__(self):
return "<%s tec=%s>" % (strclass(self.__class__), self._testFunc)
def shortDescription(self):
if self._description is not None:
return self._description
doc = self._testFunc.__doc__
return doc and doc.split("\n")[0].strip() or None
class _SubTest(TestCase):
def __init__(self, test_case, message, params):
super().__init__()
self._message = message
self.test_case = test_case
self.params = params
self.failureException = test_case.failureException
def runTest(self):
raise NotImplementedError("subtests cannot be run directly")
def _subDescription(self):
parts = []
if self._message is not _subtest_msg_sentinel:
parts.append("[{}]".format(self._message))
if self.params:
params_desc = ", ".join(
"{}={!r}".format(k, v) for (k, v) in self.params.items()
)
parts.append("({})".format(params_desc))
return " ".join(parts) or "()"
def id(self):
return "{} {}".format(self.test_case.id(), self._subDescription())
def shortDescription(self):
"""Returns a one-line description of the subtest, or None if no
description has been provided.
"""
return self.test_case.shortDescription()
def __str__(self):
return "{} {}".format(self.test_case, self._subDescription())
python-hl7-0.4.2/tests/samples.py 0000664 0000000 0000000 00000030360 14012562772 0016716 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Sample message from HL7 Normative Edition
# http://healthinfo.med.dal.ca/hl7intro/CDA_R2_normativewebedition/help/v3guide/v3guide.htm#v3gexamples
sample_hl7 = "\r".join(
[
"MSH|^~\\&|GHH LAB|ELAB-3|GHH OE|BLDG4|200202150930||ORU^R01|CNTRL-3456|P|2.4",
"PID|||555-44-4444||EVERYWOMAN^EVE^E^^^^L|JONES|196203520|F|||153 FERNWOOD DR.^^STATESVILLE^OH^35292||(206)3345232|(206)752-121||||AC555444444||67-A4335^OH^20030520",
"OBR|1|845439^GHH OE|1045813^GHH LAB|1554-5^GLUCOSE|||200202150730||||||||555-55-5555^PRIMARY^PATRICIA P^^^^MD^^LEVEL SEVEN HEALTHCARE, INC.|||||||||F||||||444-44-4444^HIPPOCRATES^HOWARD H^^^^MD",
"OBX|1|SN|1554-5^GLUCOSE^POST 12H CFST:MCNC:PT:SER/PLAS:QN||^182|mg/dl|70_105|H|||F",
"OBX|2|FN|1553-5^GLUCOSE^POST 12H CFST:MCNC:PT:SER/PLAS:QN||^182|mg/dl|70_105|H|||F\r",
]
)
# Example from: http://wiki.medical-objects.com.au/index.php/Hl7v2_parsing
rep_sample_hl7 = "\r".join(
[
"MSH|^~\\&|GHH LAB|ELAB-3|GHH OE|BLDG4|200202150930||ORU^R01|CNTRL-3456|P|2.4",
"PID|Field1|Component1^Component2|Component1^Sub-Component1&Sub-Component2^Component3|Repeat1~Repeat2",
"",
]
)
# Source: http://www.health.vic.gov.au/hdss/vinah/2006-07/appendix-a-sample-messages.pdf
sample_batch = "\r".join(
[
"BHS|^~\\&||ABCHS||AUSDHSV|20070101123401||||abchs20070101123401-1",
"MSH|^~\\&||ABCHS||AUSDHSV|20070101112951||ADT^A04^ADT_A01|12334456778890|P|2.5|||NE|NE|AU|ASCII",
"EVN|A04|20060705000000",
"PID|1||0000112234^^^100^A||XXXXXXXXXX^^^^^^S||10131113|1||4|^^RICHMOND^^3121||||1201||||||||1100|||||||||AAA",
"PD1||2",
"NK1|1||1||||||||||||||||||2",
"PV1|1|O||||^^^^^1",
"BTS|1",
"",
]
)
sample_batch1 = "\r".join(
[
"BHS|^~\\&||ABCHS||AUSDHSV|20070101123401||||abchs20070101123401-1",
"MSH|^~\\&||ABCHS||AUSDHSV|20070101112951||ADT^A04^ADT_A01|12334456778890|P|2.5|||NE|NE|AU|ASCII",
"EVN|A04|20060705000000",
"PID|1||0000112234^^^100^A||XXXXXXXXXX^^^^^^S||10131113|1||4|^^RICHMOND^^3121||||1201||||||||1100|||||||||AAA",
"PD1||2",
"NK1|1||1||||||||||||||||||2",
"PV1|1|O||||^^^^^1",
"MSH|^~\\&||ABCHS||AUSDHSV|20070101112951||ADT^A04^ADT_A01|12334456778891|P|2.5|||NE|NE|AU|ASCII",
"EVN|A04|20060705000000",
"PID|1||0000112234^^^100^A||XXXXXXXXXX^^^^^^S||10131113|1||4|^^RICHMOND^^3121||||1201||||||||1100|||||||||AAA",
"PD1||2",
"NK1|1||1||||||||||||||||||2",
"PV1|1|O||||^^^^^1",
"BTS|2",
"",
]
)
sample_batch2 = "\r".join(
[
"MSH|^~\\&||ABCHS||AUSDHSV|20070101112951||ADT^A04^ADT_A01|12334456778890|P|2.5|||NE|NE|AU|ASCII",
"EVN|A04|20060705000000",
"PID|1||0000112234^^^100^A||XXXXXXXXXX^^^^^^S||10131113|1||4|^^RICHMOND^^3121||||1201||||||||1100|||||||||AAA",
"PD1||2",
"NK1|1||1||||||||||||||||||2",
"PV1|1|O||||^^^^^1",
"MSH|^~\\&||ABCHS||AUSDHSV|20070101112951||ADT^A04^ADT_A01|12334456778891|P|2.5|||NE|NE|AU|ASCII",
"EVN|A04|20060705000000",
"PID|1||0000112234^^^100^A||XXXXXXXXXX^^^^^^S||10131113|1||4|^^RICHMOND^^3121||||1201||||||||1100|||||||||AAA",
"PD1||2",
"NK1|1||1||||||||||||||||||2",
"PV1|1|O||||^^^^^1",
"",
]
)
sample_batch3 = "\r".join(
[
"BHS|^~\\&||ABCHS||AUSDHSV|20070101123401||||abchs20070101123401-1",
"MSH|^~\\&||ABCHS||AUSDHSV|20070101112951||ADT^A04^ADT_A01|12334456778890|P|2.5|||NE|NE|AU|ASCII",
"EVN|A04|20060705000000",
"PID|1||0000112234^^^100^A||XXXXXXXXXX^^^^^^S||10131113|1||4|^^RICHMOND^^3121||||1201||||||||1100|||||||||AAA",
"PD1||2",
"NK1|1||1||||||||||||||||||2",
"PV1|1|O||||^^^^^1",
"",
]
)
sample_batch4 = "\r".join(
[
"MSH|^~\\&||ABCHS||AUSDHSV|20070101112951||ADT^A04^ADT_A01|12334456778890|P|2.5|||NE|NE|AU|ASCII",
"EVN|A04|20060705000000",
"PID|1||0000112234^^^100^A||XXXXXXXXXX^^^^^^S||10131113|1||4|^^RICHMOND^^3121||||1201||||||||1100|||||||||AAA",
"PD1||2",
"NK1|1||1||||||||||||||||||2",
"PV1|1|O||||^^^^^1",
"BTS|1",
"",
]
)
sample_bad_batch = "\r".join(
[
"BHS|^~\\&||ABCHS||AUSDHSV|20070101123401||||abchs20070101123401-1",
"EVN|A04|20060705000000",
"PID|1||0000112234^^^100^A||XXXXXXXXXX^^^^^^S||10131113|1||4|^^RICHMOND^^3121||||1201||||||||1100|||||||||AAA",
"PD1||2",
"NK1|1||1||||||||||||||||||2",
"PV1|1|O||||^^^^^1",
"BTS|1",
"",
]
)
sample_bad_batch1 = "\r".join(
[
"BHS|^~\\&||ABCHS||AUSDHSV|20070101123401||||abchs20070101123401-1",
"MSH|^~\\&||ABCHS||AUSDHSV|20070101112951||ADT^A04^ADT_A01|12334456778890|P|2.5|||NE|NE|AU|ASCII",
"EVN|A04|20060705000000",
"BHS|^~\\&||ABCHS||AUSDHSV|20070101123402||||abchs20070101123401-1",
"PID|1||0000112234^^^100^A||XXXXXXXXXX^^^^^^S||10131113|1||4|^^RICHMOND^^3121||||1201||||||||1100|||||||||AAA",
"PD1||2",
"NK1|1||1||||||||||||||||||2",
"PV1|1|O||||^^^^^1",
"BTS|1",
"",
]
)
# Source: http://www.health.vic.gov.au/hdss/vinah/2006-07/appendix-a-sample-messages.pdf
sample_file = "\r".join(
[
"FHS|^~\\&||ABCHS||AUSDHSV|20070101123401|||abchs20070101123401.hl7|",
"BHS|^~\\&||ABCHS||AUSDHSV|20070101123401||||abchs20070101123401-1",
"MSH|^~\\&||ABCHS||AUSDHSV|20070101112951||ADT^A04^ADT_A01|12334456778890|P|2.5|||NE|NE|AU|ASCII",
"EVN|A04|20060705000000",
"PID|1||0000112234^^^100^A||XXXXXXXXXX^^^^^^S||10131113|1||4|^^RICHMOND^^3121||||1201||||||||1100|||||||||AAA",
"PD1||2",
"NK1|1||1||||||||||||||||||2",
"PV1|1|O||||^^^^^1",
"BTS|1",
"FTS|1",
"",
]
)
sample_file1 = "\r".join(
[
"FHS|^~\\&||ABCHS||AUSDHSV|20070101123401|||abchs20070101123401.hl7|",
"BHS|^~\\&||ABCHS||AUSDHSV|20070101123401||||abchs20070101123401-1",
"MSH|^~\\&||ABCHS||AUSDHSV|20070101112951||ADT^A04^ADT_A01|12334456778890|P|2.5|||NE|NE|AU|ASCII",
"EVN|A04|20060705000000",
"PID|1||0000112234^^^100^A||XXXXXXXXXX^^^^^^S||10131113|1||4|^^RICHMOND^^3121||||1201||||||||1100|||||||||AAA",
"PD1||2",
"NK1|1||1||||||||||||||||||2",
"PV1|1|O||||^^^^^1",
"MSH|^~\\&||ABCHS||AUSDHSV|20070101112951||ADT^A04^ADT_A01|12334456778891|P|2.5|||NE|NE|AU|ASCII",
"EVN|A04|20060705000000",
"PID|1||0000112234^^^100^A||XXXXXXXXXX^^^^^^S||10131113|1||4|^^RICHMOND^^3121||||1201||||||||1100|||||||||AAA",
"PD1||2",
"NK1|1||1||||||||||||||||||2",
"PV1|1|O||||^^^^^1",
"BTS|2",
"BHS|^~\\&||ABCHS||AUSDHSV|20070101123401||||abchs20070101123401-2",
"MSH|^~\\&||ABCHS||AUSDHSV|20070101112951||ADT^A04^ADT_A01|12334456778890|P|2.5|||NE|NE|AU|ASCII",
"EVN|A04|20060705000000",
"PID|1||0000112234^^^100^A||XXXXXXXXXX^^^^^^S||10131113|1||4|^^RICHMOND^^3121||||1201||||||||1100|||||||||AAA",
"PD1||2",
"NK1|1||1||||||||||||||||||2",
"PV1|1|O||||^^^^^1",
"BTS|1",
"FTS|2",
"",
]
)
sample_file2 = "\r".join(
[
"FHS|^~\\&||ABCHS||AUSDHSV|20070101123401|||abchs20070101123401.hl7|",
"MSH|^~\\&||ABCHS||AUSDHSV|20070101112951||ADT^A04^ADT_A01|12334456778890|P|2.5|||NE|NE|AU|ASCII",
"EVN|A04|20060705000000",
"PID|1||0000112234^^^100^A||XXXXXXXXXX^^^^^^S||10131113|1||4|^^RICHMOND^^3121||||1201||||||||1100|||||||||AAA",
"PD1||2",
"NK1|1||1||||||||||||||||||2",
"PV1|1|O||||^^^^^1",
"MSH|^~\\&||ABCHS||AUSDHSV|20070101112951||ADT^A04^ADT_A01|12334456778891|P|2.5|||NE|NE|AU|ASCII",
"EVN|A04|20060705000000",
"PID|1||0000112234^^^100^A||XXXXXXXXXX^^^^^^S||10131113|1||4|^^RICHMOND^^3121||||1201||||||||1100|||||||||AAA",
"PD1||2",
"NK1|1||1||||||||||||||||||2",
"PV1|1|O||||^^^^^1",
"FTS|1",
"",
]
)
sample_file3 = "\r".join(
[
"FHS|^~\\&||ABCHS||AUSDHSV|20070101123401|||abchs20070101123401.hl7|",
"BHS|^~\\&||ABCHS||AUSDHSV|20070101123401||||abchs20070101123401-1",
"MSH|^~\\&||ABCHS||AUSDHSV|20070101112951||ADT^A04^ADT_A01|12334456778890|P|2.5|||NE|NE|AU|ASCII",
"EVN|A04|20060705000000",
"PID|1||0000112234^^^100^A||XXXXXXXXXX^^^^^^S||10131113|1||4|^^RICHMOND^^3121||||1201||||||||1100|||||||||AAA",
"PD1||2",
"NK1|1||1||||||||||||||||||2",
"PV1|1|O||||^^^^^1",
"BTS|1",
"",
]
)
sample_file4 = "\r".join(
[
"BHS|^~\\&||ABCHS||AUSDHSV|20070101123401||||abchs20070101123401-1",
"MSH|^~\\&||ABCHS||AUSDHSV|20070101112951||ADT^A04^ADT_A01|12334456778890|P|2.5|||NE|NE|AU|ASCII",
"EVN|A04|20060705000000",
"PID|1||0000112234^^^100^A||XXXXXXXXXX^^^^^^S||10131113|1||4|^^RICHMOND^^3121||||1201||||||||1100|||||||||AAA",
"PD1||2",
"NK1|1||1||||||||||||||||||2",
"PV1|1|O||||^^^^^1",
"BTS|1",
"FTS|1",
"",
]
)
sample_file5 = "\r".join(
[
"FHS|^~\\&||ABCHS||AUSDHSV|20070101123401|||abchs20070101123401.hl7|",
"BHS|^~\\&||ABCHS||AUSDHSV|20070101123401||||abchs20070101123401-1",
"MSH|^~\\&||ABCHS||AUSDHSV|20070101112951||ADT^A04^ADT_A01|12334456778890|P|2.5|||NE|NE|AU|ASCII",
"EVN|A04|20060705000000",
"PID|1||0000112234^^^100^A||XXXXXXXXXX^^^^^^S||10131113|1||4|^^RICHMOND^^3121||||1201||||||||1100|||||||||AAA",
"PD1||2",
"NK1|1||1||||||||||||||||||2",
"PV1|1|O||||^^^^^1",
"FTS|1",
"",
]
)
sample_file6 = "\r".join(
[
"FHS|^~\\&||ABCHS||AUSDHSV|20070101123401|||abchs20070101123401.hl7|",
"MSH|^~\\&||ABCHS||AUSDHSV|20070101112951||ADT^A04^ADT_A01|12334456778890|P|2.5|||NE|NE|AU|ASCII",
"EVN|A04|20060705000000",
"PID|1||0000112234^^^100^A||XXXXXXXXXX^^^^^^S||10131113|1||4|^^RICHMOND^^3121||||1201||||||||1100|||||||||AAA",
"PD1||2",
"NK1|1||1||||||||||||||||||2",
"PV1|1|O||||^^^^^1",
"BTS|1",
"FTS|1",
"",
]
)
sample_bad_file = "\r".join(
[
"FHS|^~\\&||ABCHS||AUSDHSV|20070101123401|||abchs20070101123401.hl7|",
"BHS|^~\\&||ABCHS||AUSDHSV|20070101123401||||abchs20070101123401-1",
"EVN|A04|20060705000000",
"PID|1||0000112234^^^100^A||XXXXXXXXXX^^^^^^S||10131113|1||4|^^RICHMOND^^3121||||1201||||||||1100|||||||||AAA",
"PD1||2",
"NK1|1||1||||||||||||||||||2",
"PV1|1|O||||^^^^^1",
"BTS|1",
"FTS|1",
"",
]
)
sample_bad_file1 = "\r".join(
[
"FHS|^~\\&||ABCHS||AUSDHSV|20070101123401|||abchs20070101123401.hl7|",
"BHS|^~\\&||ABCHS||AUSDHSV|20070101123401||||abchs20070101123401-1",
"MSH|^~\\&||ABCHS||AUSDHSV|20070101112951||ADT^A04^ADT_A01|12334456778890|P|2.5|||NE|NE|AU|ASCII",
"EVN|A04|20060705000000",
"BHS|^~\\&||ABCHS||AUSDHSV|20070101123402||||abchs20070101123401-1",
"PID|1||0000112234^^^100^A||XXXXXXXXXX^^^^^^S||10131113|1||4|^^RICHMOND^^3121||||1201||||||||1100|||||||||AAA",
"PD1||2",
"NK1|1||1||||||||||||||||||2",
"PV1|1|O||||^^^^^1",
"BTS|1",
"FTS|1",
"",
]
)
sample_bad_file2 = "\r".join(
[
"FHS|^~\\&||ABCHS||AUSDHSV|20070101123401|||abchs20070101123401.hl7|",
"BHS|^~\\&||ABCHS||AUSDHSV|20070101123401||||abchs20070101123401-1",
"MSH|^~\\&||ABCHS||AUSDHSV|20070101112951||ADT^A04^ADT_A01|12334456778890|P|2.5|||NE|NE|AU|ASCII",
"EVN|A04|20060705000000",
"FHS|^~\\&||ABCHS||AUSDHSV|20070101123402|||abchs20070101123401.hl7|",
"PID|1||0000112234^^^100^A||XXXXXXXXXX^^^^^^S||10131113|1||4|^^RICHMOND^^3121||||1201||||||||1100|||||||||AAA",
"PD1||2",
"NK1|1||1||||||||||||||||||2",
"PV1|1|O||||^^^^^1",
"BTS|1",
"FTS|1",
"",
]
)
sample_bad_file3 = "\r".join(
[
"FHS|^~\\&||ABCHS||AUSDHSV|20070101123401|||abchs20070101123401.hl7|",
"EVN|A04|20060705000000",
"PID|1||0000112234^^^100^A||XXXXXXXXXX^^^^^^S||10131113|1||4|^^RICHMOND^^3121||||1201||||||||1100|||||||||AAA",
"PD1||2",
"NK1|1||1||||||||||||||||||2",
"PV1|1|O||||^^^^^1",
"FTS|1",
"",
]
)
python-hl7-0.4.2/tests/test_accessor.py 0000664 0000000 0000000 00000001403 14012562772 0020107 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
from unittest import TestCase
from hl7 import Accessor
class AccessorTest(TestCase):
def test_key(self):
self.assertEqual("FOO", Accessor("FOO").key)
self.assertEqual("FOO2", Accessor("FOO", 2).key)
self.assertEqual("FOO2.3", Accessor("FOO", 2, 3).key)
self.assertEqual("FOO2.3.1.4.6", Accessor("FOO", 2, 3, 1, 4, 6).key)
def test_parse(self):
self.assertEqual(Accessor("FOO"), Accessor.parse_key("FOO"))
self.assertEqual(
Accessor("FOO", 2, 3, 1, 4, 6), Accessor.parse_key("FOO2.3.1.4.6")
)
def test_equality(self):
self.assertEqual(Accessor("FOO", 1, 3, 4), Accessor("FOO", 1, 3, 4))
self.assertNotEqual(Accessor("FOO", 1), Accessor("FOO", 2))
python-hl7-0.4.2/tests/test_client.py 0000664 0000000 0000000 00000020157 14012562772 0017572 0 ustar 00root root 0000000 0000000 import os
import socket
from optparse import Values
from shutil import rmtree
from tempfile import mkdtemp
from unittest import TestCase
from unittest.mock import Mock, patch
import hl7
from hl7 import __version__ as hl7_version
from hl7.client import CR, EB, SB, MLLPClient, MLLPException, mllp_send
class MLLPClientTest(TestCase):
def setUp(self):
# use a mock version of socket
self.socket_patch = patch("hl7.client.socket.socket")
self.mock_socket = self.socket_patch.start()
self.client = MLLPClient("localhost", 6666)
def tearDown(self):
# unpatch socket
self.socket_patch.stop()
def test_connect(self):
self.mock_socket.assert_called_once_with(socket.AF_INET, socket.SOCK_STREAM)
self.client.socket.connect.assert_called_once_with(("localhost", 6666))
def test_close(self):
self.client.close()
self.client.socket.close.assert_called_once_with()
def test_send(self):
self.client.socket.recv.return_value = "thanks"
result = self.client.send("foobar\n")
self.assertEqual(result, "thanks")
self.client.socket.send.assert_called_once_with("foobar\n")
self.client.socket.recv.assert_called_once_with(4096)
def test_send_message_unicode(self):
self.client.socket.recv.return_value = "thanks"
result = self.client.send_message(u"foobar")
self.assertEqual(result, "thanks")
self.client.socket.send.assert_called_once_with(b"\x0bfoobar\x1c\x0d")
def test_send_message_bytestring(self):
self.client.socket.recv.return_value = "thanks"
result = self.client.send_message(b"foobar")
self.assertEqual(result, "thanks")
self.client.socket.send.assert_called_once_with(b"\x0bfoobar\x1c\x0d")
def test_send_message_hl7_message(self):
self.client.socket.recv.return_value = "thanks"
message = hl7.parse(r"MSH|^~\&|GHH LAB|ELAB")
result = self.client.send_message(message)
self.assertEqual(result, "thanks")
self.client.socket.send.assert_called_once_with(
b"\x0bMSH|^~\\&|GHH LAB|ELAB\r\x1c\x0d"
)
def test_context_manager(self):
with MLLPClient("localhost", 6666) as client:
client.send("hello world")
self.client.socket.send.assert_called_once_with("hello world")
self.client.socket.close.assert_called_once_with()
def test_context_manager_exception(self):
with self.assertRaises(Exception):
with MLLPClient("localhost", 6666):
raise Exception()
# socket.close should be called via the with statement
self.client.socket.close.assert_called_once_with()
class MLLPSendTest(TestCase):
def setUp(self):
# patch to avoid touching sys and socket
self.socket_patch = patch("hl7.client.socket.socket")
self.mock_socket = self.socket_patch.start()
self.mock_socket().recv.return_value = "thanks"
self.stdout_patch = patch("hl7.client.stdout")
self.mock_stdout = self.stdout_patch.start()
self.stdin_patch = patch("hl7.client.stdin")
self.mock_stdin = self.stdin_patch.start()
self.stderr_patch = patch("hl7.client.stderr")
self.mock_stderr = self.stderr_patch.start()
self.exit_patch = patch("hl7.client.sys.exit")
self.mock_exit = self.exit_patch.start()
# we need a temporary directory
self.dir = mkdtemp()
self.write(SB + b"foobar" + EB + CR)
self.option_values = Values(
{
"port": 6661,
"filename": os.path.join(self.dir, "test.hl7"),
"verbose": True,
"loose": False,
"version": False,
}
)
self.options_patch = patch("hl7.client.OptionParser")
option_parser = self.options_patch.start()
self.mock_options = Mock()
option_parser.return_value = self.mock_options
self.mock_options.parse_args.return_value = (self.option_values, ["localhost"])
def tearDown(self):
# unpatch
self.socket_patch.stop()
self.options_patch.stop()
self.stdout_patch.stop()
self.stdin_patch.stop()
self.stderr_patch.stop()
self.exit_patch.stop()
# clean up the temp directory
rmtree(self.dir)
def write(self, content, path="test.hl7"):
with open(os.path.join(self.dir, path), "wb") as f:
f.write(content)
def test_send(self):
mllp_send()
self.mock_socket().connect.assert_called_once_with(("localhost", 6661))
self.mock_socket().send.assert_called_once_with(SB + b"foobar" + EB + CR)
self.mock_stdout.assert_called_once_with("thanks")
self.assertFalse(self.mock_exit.called)
def test_send_multiple(self):
self.mock_socket().recv.return_value = "thanks"
self.write(SB + b"foobar" + EB + CR + SB + b"hello" + EB + CR)
mllp_send()
self.assertEqual(
self.mock_socket().send.call_args_list[0][0][0], SB + b"foobar" + EB + CR
)
self.assertEqual(
self.mock_socket().send.call_args_list[1][0][0], SB + b"hello" + EB + CR
)
def test_leftover_buffer(self):
self.write(SB + b"foobar" + EB + CR + SB + b"stuff")
self.assertRaises(MLLPException, mllp_send)
self.mock_socket().send.assert_called_once_with(SB + b"foobar" + EB + CR)
def test_quiet(self):
self.option_values.verbose = False
mllp_send()
self.mock_socket().send.assert_called_once_with(SB + b"foobar" + EB + CR)
self.assertFalse(self.mock_stdout.called)
def test_port(self):
self.option_values.port = 7890
mllp_send()
self.mock_socket().connect.assert_called_once_with(("localhost", 7890))
def test_stdin(self):
self.option_values.filename = None
self.mock_stdin.return_value = FakeStream()
mllp_send()
self.mock_socket().send.assert_called_once_with(SB + b"hello" + EB + CR)
def test_loose_no_stdin(self):
self.option_values.loose = True
self.option_values.filename = None
self.mock_stdin.return_value = FakeStream()
mllp_send()
self.assertFalse(self.mock_socket().send.called)
self.mock_stderr().write.assert_called_with("--loose requires --file\n")
self.mock_exit.assert_called_with(1)
def test_loose_windows_newline(self):
self.option_values.loose = True
self.write(SB + b"MSH|^~\\&|foo\r\nbar\r\n" + EB + CR)
mllp_send()
self.mock_socket().send.assert_called_once_with(
SB + b"MSH|^~\\&|foo\rbar" + EB + CR
)
def test_loose_unix_newline(self):
self.option_values.loose = True
self.write(SB + b"MSH|^~\\&|foo\nbar\n" + EB + CR)
mllp_send()
self.mock_socket().send.assert_called_once_with(
SB + b"MSH|^~\\&|foo\rbar" + EB + CR
)
def test_loose_no_mllp_characters(self):
self.option_values.loose = True
self.write(b"MSH|^~\\&|foo\r\nbar\r\n")
mllp_send()
self.mock_socket().send.assert_called_once_with(
SB + b"MSH|^~\\&|foo\rbar" + EB + CR
)
def test_loose_send_mutliple(self):
self.option_values.loose = True
self.mock_socket().recv.return_value = "thanks"
self.write(b"MSH|^~\\&|1\r\nOBX|1\r\nMSH|^~\\&|2\r\nOBX|2\r\n")
mllp_send()
self.assertEqual(
self.mock_socket().send.call_args_list[0][0][0],
SB + b"MSH|^~\\&|1\rOBX|1" + EB + CR,
)
self.assertEqual(
self.mock_socket().send.call_args_list[1][0][0],
SB + b"MSH|^~\\&|2\rOBX|2" + EB + CR,
)
def test_version(self):
self.option_values.version = True
mllp_send()
self.assertFalse(self.mock_socket().connect.called)
self.mock_stdout.assert_called_once_with(str(hl7_version))
class FakeStream(object):
count = 0
def read(self, buf):
self.count += 1
if self.count == 1:
return SB + b"hello" + EB + CR
else:
return b""
python-hl7-0.4.2/tests/test_construction.py 0000664 0000000 0000000 00000003453 14012562772 0021046 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
from unittest import TestCase
import hl7
from .samples import rep_sample_hl7
SEP = r"|^~\&"
CR_SEP = "\r"
class ConstructionTest(TestCase):
def test_create_msg(self):
# Create a message
MSH = hl7.Segment(SEP[0], [hl7.Field(SEP[1], ["MSH"])])
MSA = hl7.Segment(SEP[0], [hl7.Field(SEP[1], ["MSA"])])
response = hl7.Message(CR_SEP, [MSH, MSA])
response["MSH.F1.R1"] = SEP[0]
response["MSH.F2.R1"] = SEP[1:]
self.assertEqual(str(response), "MSH|^~\\&|\rMSA\r")
def test_append(self):
# Append a segment to a message
MSH = hl7.Segment(SEP[0], [hl7.Field(SEP[1], ["MSH"])])
response = hl7.Message(CR_SEP, [MSH])
response["MSH.F1.R1"] = SEP[0]
response["MSH.F2.R1"] = SEP[1:]
MSA = hl7.Segment(SEP[0], [hl7.Field(SEP[1], ["MSA"])])
response.append(MSA)
self.assertEqual(str(response), "MSH|^~\\&|\rMSA\r")
def test_append_from_source(self):
# Copy a segment between messages
MSH = hl7.Segment(SEP[0], [hl7.Field(SEP[1], ["MSH"])])
MSA = hl7.Segment(SEP[0], [hl7.Field(SEP[1], ["MSA"])])
response = hl7.Message(CR_SEP, [MSH, MSA])
response["MSH.F1.R1"] = SEP[0]
response["MSH.F2.R1"] = SEP[1:]
self.assertEqual(str(response), "MSH|^~\\&|\rMSA\r")
src_msg = hl7.parse(rep_sample_hl7)
PID = src_msg["PID"][0]
self.assertEqual(
str(PID),
"PID|Field1|Component1^Component2|Component1^Sub-Component1&Sub-Component2^Component3|Repeat1~Repeat2",
)
response.append(PID)
self.assertEqual(
str(response),
"MSH|^~\\&|\rMSA\rPID|Field1|Component1^Component2|Component1^Sub-Component1&Sub-Component2^Component3|Repeat1~Repeat2\r",
)
python-hl7-0.4.2/tests/test_containers.py 0000664 0000000 0000000 00000011014 14012562772 0020451 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
from unittest import TestCase
import hl7
from hl7 import Field, Segment
from .samples import sample_hl7
class ContainerTest(TestCase):
def test_unicode(self):
msg = hl7.parse(sample_hl7)
self.assertEqual(str(msg), sample_hl7)
self.assertEqual(
str(msg[3][3]), "1554-5^GLUCOSE^POST 12H CFST:MCNC:PT:SER/PLAS:QN"
)
def test_container_unicode(self):
c = hl7.Container("|")
c.extend(["1", "b", "data"])
self.assertEqual(str(c), "1|b|data")
class MessageTest(TestCase):
def test_segments(self):
msg = hl7.parse(sample_hl7)
s = msg.segments("OBX")
self.assertEqual(len(s), 2)
self.assertIsInstance(s[0], Segment)
self.assertEqual(s[0][0:3], [["OBX"], ["1"], ["SN"]])
self.assertEqual(s[1][0:3], [["OBX"], ["2"], ["FN"]])
self.assertIsInstance(s[0][1], Field)
def test_segments_does_not_exist(self):
msg = hl7.parse(sample_hl7)
self.assertRaises(KeyError, msg.segments, "BAD")
def test_segment(self):
msg = hl7.parse(sample_hl7)
s = msg.segment("OBX")
self.assertEqual(s[0:3], [["OBX"], ["1"], ["SN"]])
def test_segment_does_not_exist(self):
msg = hl7.parse(sample_hl7)
self.assertRaises(KeyError, msg.segment, "BAD")
def test_segments_dict_key(self):
msg = hl7.parse(sample_hl7)
s = msg["OBX"]
self.assertEqual(len(s), 2)
self.assertEqual(s[0][0:3], [["OBX"], ["1"], ["SN"]])
self.assertEqual(s[1][0:3], [["OBX"], ["2"], ["FN"]])
def test_MSH_1_field(self):
msg = hl7.parse(sample_hl7)
f = msg["MSH.1"]
self.assertEqual(len(f), 1)
self.assertEqual(f, "|")
def test_MSH_2_field(self):
msg = hl7.parse(sample_hl7)
f = msg["MSH.2"]
self.assertEqual(len(f), 4)
self.assertEqual(f, "^~\\&")
def test_get_slice(self):
msg = hl7.parse(sample_hl7)
s = msg.segments("OBX")[0]
self.assertIsInstance(s, Segment)
self.assertIsInstance(s[0:3], Segment)
def test_ack(self):
msg = hl7.parse(sample_hl7)
ack = msg.create_ack()
self.assertEqual(msg["MSH.1"], ack["MSH.1"])
self.assertEqual(msg["MSH.2"], ack["MSH.2"])
self.assertEqual("ACK", ack["MSH.9.1.1"])
self.assertEqual(msg["MSH.9.1.2"], ack["MSH.9.1.2"])
self.assertEqual("ACK", ack["MSH.9.1.3"])
self.assertNotEqual(msg["MSH.7"], ack["MSH.7"])
self.assertNotEqual(msg["MSH.10"], ack["MSH.10"])
self.assertEqual("AA", ack["MSA.1"])
self.assertEqual(msg["MSH.10"], ack["MSA.2"])
self.assertEqual(20, len(ack["MSH.10"]))
self.assertEqual(msg["MSH.5"], ack["MSH.3"])
self.assertEqual(msg["MSH.6"], ack["MSH.4"])
self.assertEqual(msg["MSH.3"], ack["MSH.5"])
self.assertEqual(msg["MSH.4"], ack["MSH.6"])
ack2 = msg.create_ack(
ack_code="AE", message_id="testid", application="python", facility="test"
)
self.assertEqual("AE", ack2["MSA.1"])
self.assertEqual("testid", ack2["MSH.10"])
self.assertEqual("python", ack2["MSH.3"])
self.assertEqual("test", ack2["MSH.4"])
self.assertNotEqual(ack["MSH.10"], ack2["MSH.10"])
class TestMessage(hl7.Message):
pass
class TestSegment(hl7.Segment):
pass
class TestField(hl7.Field):
pass
class TestRepetition(hl7.Repetition):
pass
class TestComponent(hl7.Component):
pass
class TestFactory(hl7.Factory):
create_message = TestMessage
create_segment = TestSegment
create_field = TestField
create_repetition = TestRepetition
create_component = TestComponent
class FactoryTest(TestCase):
def test_parse(self):
msg = hl7.parse(sample_hl7, factory=TestFactory)
self.assertIsInstance(msg, TestMessage)
s = msg.segments("OBX")
self.assertIsInstance(s[0], TestSegment)
self.assertIsInstance(s[0](3), TestField)
self.assertIsInstance(s[0](3)(1), TestRepetition)
self.assertIsInstance(s[0](3)(1)(1), TestComponent)
self.assertEqual("1554-5", s[0](3)(1)(1)(1))
def test_ack(self):
msg = hl7.parse(sample_hl7, factory=TestFactory)
ack = msg.create_ack()
self.assertIsInstance(ack, TestMessage)
self.assertIsInstance(ack(1)(9), TestField)
self.assertIsInstance(ack(1)(9)(1), TestRepetition)
self.assertIsInstance(ack(1)(9)(1)(2), TestComponent)
self.assertEqual("R01", ack(1)(9)(1)(2)(1))
python-hl7-0.4.2/tests/test_datetime.py 0000664 0000000 0000000 00000002762 14012562772 0020112 0 ustar 00root root 0000000 0000000 from datetime import datetime
from unittest import TestCase
from hl7.datatypes import _UTCOffset, parse_datetime
class DatetimeTest(TestCase):
def test_parse_date(self):
self.assertEqual(datetime(1901, 2, 13), parse_datetime("19010213"))
def test_parse_datetime(self):
self.assertEqual(
datetime(2014, 3, 11, 14, 25, 33), parse_datetime("20140311142533")
)
def test_parse_datetime_frac(self):
self.assertEqual(
datetime(2014, 3, 11, 14, 25, 33, 100000),
parse_datetime("20140311142533.1"),
)
self.assertEqual(
datetime(2014, 3, 11, 14, 25, 33, 10000),
parse_datetime("20140311142533.01"),
)
self.assertEqual(
datetime(2014, 3, 11, 14, 25, 33, 1000),
parse_datetime("20140311142533.001"),
)
self.assertEqual(
datetime(2014, 3, 11, 14, 25, 33, 100),
parse_datetime("20140311142533.0001"),
)
def test_parse_tz(self):
self.assertEqual(
datetime(2014, 3, 11, 14, 12, tzinfo=_UTCOffset(330)),
parse_datetime("201403111412+0530"),
)
self.assertEqual(
datetime(2014, 3, 11, 14, 12, 20, tzinfo=_UTCOffset(-300)),
parse_datetime("20140311141220-0500"),
)
def test_tz(self):
self.assertEqual("+0205", _UTCOffset(125).tzname(datetime.utcnow()))
self.assertEqual("-0410", _UTCOffset(-250).tzname(datetime.utcnow()))
python-hl7-0.4.2/tests/test_mllp.py 0000664 0000000 0000000 00000005440 14012562772 0017256 0 ustar 00root root 0000000 0000000 import asyncio
import asyncio.streams
import sys
from unittest.mock import create_autospec
import hl7
import hl7.mllp
# IsolatedAsyncioTestCase added in 3.8, use backport for older code
if sys.version_info.major <= 3 and sys.version_info.minor < 8:
from .backports.unittest.async_case import IsolatedAsyncioTestCase
else:
from unittest import IsolatedAsyncioTestCase
START_BLOCK = b"\x0b"
END_BLOCK = b"\x1c"
CARRIAGE_RETURN = b"\x0d"
class MLLPStreamWriterTest(IsolatedAsyncioTestCase):
def setUp(self):
self.transport = create_autospec(asyncio.Transport)
async def asyncSetUp(self):
self.writer = hl7.mllp.MLLPStreamWriter(
self.transport,
create_autospec(asyncio.streams.StreamReaderProtocol),
create_autospec(hl7.mllp.MLLPStreamReader),
asyncio.get_running_loop(),
)
def test_writeblock(self):
self.writer.writeblock(b"foobar")
self.transport.write.assert_called_with(
START_BLOCK + b"foobar" + END_BLOCK + CARRIAGE_RETURN
)
class MLLPStreamReaderTest(IsolatedAsyncioTestCase):
def setUp(self):
self.reader = hl7.mllp.MLLPStreamReader()
async def test_readblock(self):
self.reader.feed_data(START_BLOCK + b"foobar" + END_BLOCK + CARRIAGE_RETURN)
block = await self.reader.readblock()
self.assertEqual(block, b"foobar")
class HL7StreamWriterTest(IsolatedAsyncioTestCase):
def setUp(self):
self.transport = create_autospec(asyncio.Transport)
async def asyncSetUp(self):
def mock_cb(reader, writer):
pass
reader = create_autospec(asyncio.streams.StreamReader)
self.writer = hl7.mllp.HL7StreamWriter(
self.transport,
hl7.mllp.HL7StreamProtocol(reader, mock_cb, asyncio.get_running_loop()),
reader,
asyncio.get_running_loop(),
)
def test_writemessage(self):
message = r"MSH|^~\&|LABADT|DH|EPICADT|DH|201301011228||ACK^A01^ACK|HL7ACK00001|P|2.3\r"
message += "MSA|AA|HL7MSG00001\r"
hl7_message = hl7.parse(message)
self.writer.writemessage(hl7_message)
self.transport.write.assert_called_with(
START_BLOCK + message.encode() + END_BLOCK + CARRIAGE_RETURN
)
class HL7StreamReaderTest(IsolatedAsyncioTestCase):
def setUp(self):
self.reader = hl7.mllp.HL7StreamReader()
async def test_readblock(self):
message = r"MSH|^~\&|LABADT|DH|EPICADT|DH|201301011228||ACK^A01^ACK|HL7ACK00001|P|2.3\r"
message += "MSA|AA|HL7MSG00001\r"
self.reader.feed_data(
START_BLOCK + message.encode() + END_BLOCK + CARRIAGE_RETURN
)
hl7_message = await self.reader.readmessage()
self.assertEqual(str(hl7_message), str(hl7.parse(message)))
python-hl7-0.4.2/tests/test_parse.py 0000664 0000000 0000000 00000041466 14012562772 0017434 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
from unittest import TestCase
import hl7
from hl7 import Accessor, Component, Field, Message, ParseException, Repetition, Segment
from .samples import (
rep_sample_hl7,
sample_bad_batch,
sample_bad_batch1,
sample_bad_file,
sample_bad_file1,
sample_bad_file2,
sample_bad_file3,
sample_batch,
sample_batch1,
sample_batch2,
sample_batch3,
sample_batch4,
sample_file,
sample_file1,
sample_file2,
sample_file3,
sample_file4,
sample_file5,
sample_file6,
sample_hl7,
)
class ParseTest(TestCase):
def test_parse(self):
msg = hl7.parse(sample_hl7)
self.assertEqual(len(msg), 5)
self.assertIsInstance(msg[0][0][0], str)
self.assertEqual(msg[0][0][0], "MSH")
self.assertEqual(msg[3][0][0], "OBX")
self.assertEqual(
msg[3][3],
[[["1554-5"], ["GLUCOSE"], ["POST 12H CFST:MCNC:PT:SER/PLAS:QN"]]],
)
# Make sure MSH-1 and MSH-2 are valid
self.assertEqual(msg[0][1][0], "|")
self.assertIsInstance(msg[0][1], hl7.Field)
self.assertEqual(msg[0][2][0], r"^~\&")
self.assertIsInstance(msg[0][2], hl7.Field)
# MSH-9 is the message type
self.assertEqual(msg[0][9], [[["ORU"], ["R01"]]])
# Do it twice to make sure text coercion is idempotent
self.assertEqual(str(msg), sample_hl7)
self.assertEqual(str(msg), sample_hl7)
def test_parse_batch(self):
batch = hl7.parse_batch(sample_batch)
self.assertEqual(len(batch), 1)
self.assertIsInstance(batch[0], hl7.Message)
self.assertIsInstance(batch.header, hl7.Segment)
self.assertEqual(batch.header[0][0], "BHS")
self.assertEqual(batch.header[4][0], "ABCHS")
self.assertIsInstance(batch.trailer, hl7.Segment)
self.assertEqual(batch.trailer[0][0], "BTS")
self.assertEqual(batch.trailer[1][0], "1")
def test_parse_batch1(self):
batch = hl7.parse_batch(sample_batch1)
self.assertEqual(len(batch), 2)
self.assertIsInstance(batch[0], hl7.Message)
self.assertEqual(batch[0][0][10][0], "12334456778890")
self.assertIsInstance(batch[1], hl7.Message)
self.assertEqual(batch[1][0][10][0], "12334456778891")
self.assertIsInstance(batch.header, hl7.Segment)
self.assertEqual(batch.header[0][0], "BHS")
self.assertEqual(batch.header[4][0], "ABCHS")
self.assertIsInstance(batch.trailer, hl7.Segment)
self.assertEqual(batch.trailer[0][0], "BTS")
self.assertEqual(batch.trailer[1][0], "2")
def test_parse_batch2(self):
batch = hl7.parse_batch(sample_batch2)
self.assertEqual(len(batch), 2)
self.assertIsInstance(batch[0], hl7.Message)
self.assertEqual(batch[0][0][10][0], "12334456778890")
self.assertIsInstance(batch[1], hl7.Message)
self.assertEqual(batch[1][0][10][0], "12334456778891")
self.assertFalse(batch.header)
self.assertFalse(batch.trailer)
def test_parse_batch3(self):
batch = hl7.parse_batch(sample_batch3)
self.assertEqual(len(batch), 1)
self.assertIsInstance(batch[0], hl7.Message)
self.assertIsInstance(batch.header, hl7.Segment)
self.assertEqual(batch.header[0][0], "BHS")
self.assertEqual(batch.header[4][0], "ABCHS")
self.assertIsInstance(batch.trailer, hl7.Segment)
self.assertEqual(batch.trailer[0][0], "BTS")
def test_parse_batch4(self):
batch = hl7.parse_batch(sample_batch4)
self.assertEqual(len(batch), 1)
self.assertIsInstance(batch[0], hl7.Message)
self.assertIsNone(batch.header)
self.assertIsNone(batch.trailer)
def test_parse_bad_batch(self):
with self.assertRaises(ParseException) as cm:
hl7.parse_batch(sample_bad_batch)
self.assertIn("Segment received before message header", cm.exception.args[0])
def test_parse_bad_batch1(self):
with self.assertRaises(ParseException) as cm:
hl7.parse_batch(sample_bad_batch1)
self.assertIn(
"Batch cannot have more than one BHS segment", cm.exception.args[0]
)
def test_parse_file(self):
file = hl7.parse_file(sample_file)
self.assertEqual(len(file), 1)
self.assertIsInstance(file[0], hl7.Batch)
self.assertIsInstance(file.header, hl7.Segment)
self.assertEqual(file.header[0][0], "FHS")
self.assertEqual(file.header[4][0], "ABCHS")
self.assertIsInstance(file.trailer, hl7.Segment)
self.assertEqual(file.trailer[0][0], "FTS")
self.assertEqual(file.trailer[1][0], "1")
def test_parse_file1(self):
file = hl7.parse_file(sample_file1)
self.assertEqual(len(file), 2)
self.assertIsInstance(file[0], hl7.Batch)
self.assertEqual(file[0].trailer[1][0], "2")
self.assertIsInstance(file[1], hl7.Batch)
self.assertEqual(file[1].trailer[1][0], "1")
self.assertNotEqual(file[0], file[1])
self.assertIsInstance(file.header, hl7.Segment)
self.assertEqual(file.header[0][0], "FHS")
self.assertEqual(file.header[4][0], "ABCHS")
self.assertIsInstance(file.trailer, hl7.Segment)
self.assertEqual(file.trailer[0][0], "FTS")
self.assertEqual(file.trailer[1][0], "2")
def test_parse_file2(self):
file = hl7.parse_file(sample_file2)
self.assertEqual(len(file), 1)
self.assertIsInstance(file[0], hl7.Batch)
self.assertIsInstance(file.header, hl7.Segment)
self.assertEqual(file.header[0][0], "FHS")
self.assertEqual(file.header[4][0], "ABCHS")
self.assertIsInstance(file.trailer, hl7.Segment)
self.assertEqual(file.trailer[0][0], "FTS")
self.assertEqual(file.trailer[1][0], "1")
def test_parse_file3(self):
file = hl7.parse_file(sample_file3)
self.assertEqual(len(file), 1)
self.assertIsInstance(file[0], hl7.Batch)
self.assertIsInstance(file.header, hl7.Segment)
self.assertEqual(file.header[0][0], "FHS")
self.assertEqual(file.header[4][0], "ABCHS")
self.assertIsInstance(file.trailer, hl7.Segment)
self.assertEqual(file.trailer[0][0], "FTS")
def test_parse_file4(self):
file = hl7.parse_file(sample_file4)
self.assertEqual(len(file), 1)
self.assertIsInstance(file[0], hl7.Batch)
self.assertIsNone(file.header)
self.assertIsNone(file.trailer)
def test_parse_file5(self):
file = hl7.parse_file(sample_file5)
self.assertEqual(len(file), 1)
self.assertIsInstance(file[0], hl7.Batch)
self.assertIsInstance(file.header, hl7.Segment)
self.assertEqual(file.header[0][0], "FHS")
self.assertEqual(file.header[4][0], "ABCHS")
self.assertIsInstance(file.trailer, hl7.Segment)
self.assertEqual(file.trailer[0][0], "FTS")
self.assertEqual(file.trailer[1][0], "1")
def test_parse_file6(self):
file = hl7.parse_file(sample_file6)
self.assertEqual(len(file), 1)
self.assertIsInstance(file[0], hl7.Batch)
self.assertIsInstance(file.header, hl7.Segment)
self.assertEqual(file.header[0][0], "FHS")
self.assertEqual(file.header[4][0], "ABCHS")
self.assertIsInstance(file.trailer, hl7.Segment)
self.assertEqual(file.trailer[0][0], "FTS")
self.assertEqual(file.trailer[1][0], "1")
def test_parse_bad_file(self):
with self.assertRaises(ParseException) as cm:
hl7.parse_file(sample_bad_file)
self.assertIn("Segment received before message header", cm.exception.args[0])
def test_parse_bad_file1(self):
with self.assertRaises(ParseException) as cm:
hl7.parse_file(sample_bad_file1)
self.assertIn(
"Batch cannot have more than one BHS segment", cm.exception.args[0]
)
def test_parse_bad_file2(self):
with self.assertRaises(ParseException) as cm:
hl7.parse_file(sample_bad_file2)
self.assertIn(
"File cannot have more than one FHS segment", cm.exception.args[0]
)
def test_parse_bad_file3(self):
with self.assertRaises(ParseException) as cm:
hl7.parse_file(sample_bad_file3)
self.assertIn("Segment received before message header", cm.exception.args[0])
def test_parse_hl7(self):
obj = hl7.parse_hl7(sample_hl7)
self.assertIsInstance(obj, hl7.Message)
obj = hl7.parse_hl7(sample_batch)
self.assertIsInstance(obj, hl7.Batch)
obj = hl7.parse_hl7(sample_batch1)
self.assertIsInstance(obj, hl7.Batch)
obj = hl7.parse_hl7(sample_batch2)
self.assertIsInstance(obj, hl7.Batch)
obj = hl7.parse_hl7(sample_file)
self.assertIsInstance(obj, hl7.File)
obj = hl7.parse_hl7(sample_file1)
self.assertIsInstance(obj, hl7.File)
obj = hl7.parse_hl7(sample_file2)
self.assertIsInstance(obj, hl7.File)
def test_bytestring_converted_to_unicode(self):
msg = hl7.parse(str(sample_hl7))
self.assertEqual(len(msg), 5)
self.assertIsInstance(msg[0][0][0], str)
self.assertEqual(msg[0][0][0], "MSH")
def test_non_ascii_bytestring(self):
# \x96 - valid cp1252, not valid utf8
# it is the responsibility of the caller to convert to unicode
msg = hl7.parse(b"MSH|^~\\&|GHH LAB|ELAB\x963", encoding="cp1252")
self.assertEqual(msg[0][4][0], "ELAB\u20133")
def test_non_ascii_bytestring_no_encoding(self):
# \x96 - valid cp1252, not valid utf8
# it is the responsibility of the caller to convert to unicode
self.assertRaises(UnicodeDecodeError, hl7.parse, b"MSH|^~\\&|GHH LAB|ELAB\x963")
def test_parsing_classes(self):
msg = hl7.parse(sample_hl7)
self.assertIsInstance(msg, hl7.Message)
self.assertIsInstance(msg[3], hl7.Segment)
self.assertIsInstance(msg[3][0], hl7.Field)
self.assertIsInstance(msg[3][0][0], str)
def test_nonstandard_separators(self):
nonstd = "MSH$%~\\&$GHH LAB\rPID$$$555-44-4444$$EVERYWOMAN%EVE%E%%%L\r"
msg = hl7.parse(nonstd)
self.assertEqual(str(msg), nonstd)
self.assertEqual(len(msg), 2)
self.assertEqual(
msg[1][5], [[["EVERYWOMAN"], ["EVE"], ["E"], [""], [""], ["L"]]]
)
def test_repetition(self):
msg = hl7.parse(rep_sample_hl7)
self.assertEqual(msg[1][4], [["Repeat1"], ["Repeat2"]])
self.assertIsInstance(msg[1][4], Field)
self.assertIsInstance(msg[1][4][0], Repetition)
self.assertIsInstance(msg[1][4][1], Repetition)
self.assertEqual(str(msg[1][4][0][0]), "Repeat1")
self.assertIsInstance(msg[1][4][0][0], str)
self.assertEqual(str(msg[1][4][1][0]), "Repeat2")
self.assertIsInstance(msg[1][4][1][0], str)
def test_empty_initial_repetition(self):
# Switch to look like "|~Repeat2|
msg = hl7.parse(rep_sample_hl7.replace("Repeat1", ""))
self.assertEqual(msg[1][4], [[""], ["Repeat2"]])
def test_subcomponent(self):
msg = hl7.parse(rep_sample_hl7)
self.assertEqual(
msg[1][3],
[[["Component1"], ["Sub-Component1", "Sub-Component2"], ["Component3"]]],
)
def test_elementnumbering(self):
# Make sure that the numbering of repetitions. components and
# sub-components is indexed from 1 when invoked as callable
# (for compatibility with HL7 spec numbering)
# and not 0-based (default for Python list)
msg = hl7.parse(rep_sample_hl7)
f = msg(2)(3)(1)(2)(2)
self.assertIs(f, msg["PID.3.1.2.2"])
self.assertIs(f, msg[1][3][0][1][1])
f = msg(2)(4)(2)(1)
self.assertIs(f, msg["PID.4.2.1"])
self.assertIs(f, msg[1][4][1][0])
# Repetition level accessed in list-form doesn't make much sense...
self.assertIs(f, msg["PID.4.2"])
def test_extract(self):
msg = hl7.parse(rep_sample_hl7)
# Full correct path
self.assertEqual(msg["PID.3.1.2.2"], "Sub-Component2")
self.assertEqual(msg[Accessor("PID", 1, 3, 1, 2, 2)], "Sub-Component2")
# Shorter Paths
self.assertEqual(msg["PID.1.1"], "Field1")
self.assertEqual(msg[Accessor("PID", 1, 1, 1)], "Field1")
self.assertEqual(msg["PID.1"], "Field1")
self.assertEqual(msg["PID1.1"], "Field1")
self.assertEqual(msg["PID.3.1.2"], "Sub-Component1")
# Longer Paths
self.assertEqual(msg["PID.1.1.1.1"], "Field1")
# Incorrect path
self.assertRaisesRegex(
IndexError,
"PID.1.1.1.2",
msg.extract_field,
*Accessor.parse_key("PID.1.1.1.2")
)
# Optional field, not included in message
self.assertEqual(msg["MSH.20"], "")
# Optional sub-component, not included in message
self.assertEqual(msg["PID.3.1.2.3"], "")
self.assertEqual(msg["PID.3.1.3"], "Component3")
self.assertEqual(msg["PID.3.1.4"], "")
def test_assign(self):
msg = hl7.parse(rep_sample_hl7)
# Field
msg["MSH.20"] = "FIELD 20"
self.assertEqual(msg["MSH.20"], "FIELD 20")
# Component
msg["MSH.21.1.1"] = "COMPONENT 21.1.1"
self.assertEqual(msg["MSH.21.1.1"], "COMPONENT 21.1.1")
# Sub-Component
msg["MSH.21.1.2.4"] = "SUBCOMPONENT 21.1.2.4"
self.assertEqual(msg["MSH.21.1.2.4"], "SUBCOMPONENT 21.1.2.4")
# Verify round-tripping (i.e. that separators are correct)
msg2 = hl7.parse(str(msg))
self.assertEqual(msg2["MSH.20"], "FIELD 20")
self.assertEqual(msg2["MSH.21.1.1"], "COMPONENT 21.1.1")
self.assertEqual(msg2["MSH.21.1.2.4"], "SUBCOMPONENT 21.1.2.4")
def test_unescape(self):
msg = hl7.parse(rep_sample_hl7)
# Replace Separators
self.assertEqual(msg.unescape("\\E\\"), "\\")
self.assertEqual(msg.unescape("\\F\\"), "|")
self.assertEqual(msg.unescape("\\S\\"), "^")
self.assertEqual(msg.unescape("\\T\\"), "&")
self.assertEqual(msg.unescape("\\R\\"), "~")
# Replace Highlighting
self.assertEqual(msg.unescape("\\H\\text\\N\\"), "_text_")
# Application Overrides
self.assertEqual(msg.unescape("\\H\\text\\N\\", {"H": "*", "N": "*"}), "*text*")
# Hex Codes
self.assertEqual(msg.unescape("\\X20202020\\"), " ")
self.assertEqual(msg.unescape("\\Xe1\\\\Xe9\\\\Xed\\\\Xf3\\\\Xfa\\"), "áéíóú")
def test_escape(self):
msg = hl7.parse(rep_sample_hl7)
# Escape Separators
self.assertEqual(msg.escape("\\"), "\\E\\")
self.assertEqual(msg.escape("|"), "\\F\\")
self.assertEqual(msg.escape("^"), "\\S\\")
self.assertEqual(msg.escape("&"), "\\T\\")
self.assertEqual(msg.escape("~"), "\\R\\")
# Escape ASCII characters
self.assertEqual(msg.escape("asdf"), "asdf")
# Escape non-ASCII characters
self.assertEqual(msg.escape("áéíóú"), "\\Xe1\\\\Xe9\\\\Xed\\\\Xf3\\\\Xfa\\")
self.assertEqual(msg.escape("äsdf"), "\\Xe4\\sdf")
def test_file(self):
# Extract message from file
self.assertTrue(hl7.isfile(sample_file))
messages = hl7.split_file(sample_file)
self.assertEqual(len(messages), 1)
# message can be parsed
msg = hl7.parse(messages[0])
# message has expected content
self.assertEqual(
[s[0][0] for s in msg], ["MSH", "EVN", "PID", "PD1", "NK1", "PV1"]
)
class ParsePlanTest(TestCase):
def test_create_parse_plan(self):
plan = hl7.parser.create_parse_plan(sample_hl7)
self.assertEqual(plan.separators, ["\r", "|", "~", "^", "&"])
self.assertEqual(
plan.containers, [Message, Segment, Field, Repetition, Component]
)
def test_parse_plan(self):
plan = hl7.parser.create_parse_plan(sample_hl7)
self.assertEqual(plan.separator, "\r")
con = plan.container([1, 2])
self.assertIsInstance(con, Message)
self.assertEqual(con, [1, 2])
self.assertEqual(con.separator, "\r")
def test_parse_plan_next(self):
plan = hl7.parser.create_parse_plan(sample_hl7)
n1 = plan.next()
self.assertEqual(n1.separators, ["|", "~", "^", "&"])
self.assertEqual(n1.containers, [Segment, Field, Repetition, Component])
n2 = n1.next()
self.assertEqual(n2.separators, ["~", "^", "&"])
self.assertEqual(n2.containers, [Field, Repetition, Component])
n3 = n2.next()
self.assertEqual(n3.separators, ["^", "&"])
self.assertEqual(n3.containers, [Repetition, Component])
n4 = n3.next()
self.assertEqual(n4.separators, ["&"])
self.assertEqual(n4.containers, [Component])
n5 = n4.next()
self.assertTrue(n5 is None)
python-hl7-0.4.2/tests/test_util.py 0000664 0000000 0000000 00000003606 14012562772 0017271 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
from unittest import TestCase
import hl7
from .samples import (
sample_batch,
sample_batch1,
sample_batch2,
sample_file,
sample_file1,
sample_file2,
sample_hl7,
)
class IsHL7Test(TestCase):
def test_ishl7(self):
self.assertTrue(hl7.ishl7(sample_hl7))
self.assertFalse(hl7.ishl7(sample_batch))
self.assertFalse(hl7.ishl7(sample_batch1))
self.assertFalse(hl7.ishl7(sample_batch2))
self.assertFalse(hl7.ishl7(sample_file))
self.assertFalse(hl7.ishl7(sample_file1))
self.assertFalse(hl7.ishl7(sample_file2))
def test_ishl7_empty(self):
self.assertFalse(hl7.ishl7(""))
def test_ishl7_None(self):
self.assertFalse(hl7.ishl7(None))
def test_ishl7_wrongsegment(self):
message = "OBX|1|SN|1554-5^GLUCOSE^POST 12H CFST:MCNC:PT:SER/PLAS:QN||^182|mg/dl|70_105|H|||F\r"
self.assertFalse(hl7.ishl7(message))
def test_isbatch(self):
self.assertFalse(hl7.ishl7(sample_batch))
self.assertFalse(hl7.ishl7(sample_batch1))
self.assertFalse(hl7.ishl7(sample_batch2))
self.assertTrue(hl7.isbatch(sample_batch))
self.assertTrue(hl7.isbatch(sample_batch1))
self.assertTrue(hl7.isbatch(sample_batch2))
def test_isfile(self):
self.assertFalse(hl7.ishl7(sample_file))
self.assertFalse(hl7.ishl7(sample_file1))
self.assertFalse(hl7.ishl7(sample_file2))
self.assertFalse(hl7.isbatch(sample_file))
self.assertFalse(hl7.isbatch(sample_file1))
self.assertFalse(hl7.isbatch(sample_file2))
self.assertTrue(hl7.isfile(sample_file))
self.assertTrue(hl7.isfile(sample_file1))
self.assertTrue(hl7.isfile(sample_file2))
self.assertTrue(hl7.isfile(sample_batch))
self.assertTrue(hl7.isfile(sample_batch1))
self.assertTrue(hl7.isfile(sample_batch2))
python-hl7-0.4.2/tests/test_version.py 0000664 0000000 0000000 00000002467 14012562772 0020005 0 ustar 00root root 0000000 0000000 from unittest import TestCase
from unittest.mock import patch
from hl7.version import get_version
class GetVersionTest(TestCase):
@patch("hl7.version.VERSION", new=(0, 4, 1))
def test_no_modifier(self):
self.assertEqual("0.4.1", get_version())
@patch("hl7.version.VERSION", new=(0, 4, 1, ""))
def test_empty_modifier(self):
self.assertEqual("0.4.1", get_version())
@patch("hl7.version.VERSION", new=(0, 4, 1, None))
def test_none_modifier(self):
self.assertEqual("0.4.1", get_version())
@patch("hl7.version.VERSION", new=(0, 4, 1, "final"))
def test_final(self):
self.assertEqual("0.4.1", get_version())
@patch("hl7.version.VERSION", new=(0, 4, 1, "rc"))
def test_rc(self):
self.assertEqual("0.4.1rc", get_version())
@patch("hl7.version.VERSION", new=(0, 4, 1, "rc4"))
def test_rc_num(self):
self.assertEqual("0.4.1rc4", get_version())
@patch("hl7.version.VERSION", new=(0, 4, 1, "b"))
def test_beta(self):
self.assertEqual("0.4.1b", get_version())
@patch("hl7.version.VERSION", new=(0, 4, 1, "a"))
def test_alpha(self):
self.assertEqual("0.4.1a", get_version())
@patch("hl7.version.VERSION", new=(0, 4, 1, "dev"))
def test_dev(self):
self.assertEqual("0.4.1.dev", get_version())
python-hl7-0.4.2/tox.ini 0000664 0000000 0000000 00000000670 14012562772 0015052 0 ustar 00root root 0000000 0000000 [tox]
envlist =
py39, py38, py37, py36, py35, docs
[testenv]
commands =
python -m unittest discover -t . -s tests
[testenv:py35]
basepython = python3.5
[testenv:py36]
basepython = python3.6
[testenv:py37]
basepython = python3.7
[testenv:py38]
basepython = python3.8
[testenv:py39]
basepython = python3.9
[testenv:docs]
whitelist_externals = make
deps =
-r{toxinidir}/requirements.txt
commands =
make clean-docs docs