pax_global_header00006660000000000000000000000064140125627720014520gustar00rootroot0000000000000052 comment=0af8b5111c7d4471c4d9a45dbbb9c57376d435b8 python-hl7-0.4.2/000077500000000000000000000000001401256277200135345ustar00rootroot00000000000000python-hl7-0.4.2/.coveragerc000066400000000000000000000000451401256277200156540ustar00rootroot00000000000000[run] branch = True omit = env/*python-hl7-0.4.2/.flake8000066400000000000000000000004151401256277200147070ustar00rootroot00000000000000# -*- 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, distpython-hl7-0.4.2/.github/000077500000000000000000000000001401256277200150745ustar00rootroot00000000000000python-hl7-0.4.2/.github/workflows/000077500000000000000000000000001401256277200171315ustar00rootroot00000000000000python-hl7-0.4.2/.github/workflows/codeql-analysis.yml000066400000000000000000000050321401256277200227440ustar00rootroot00000000000000# 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.yaml000066400000000000000000000030751401256277200210010ustar00rootroot00000000000000name: 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/.gitignore000066400000000000000000000001611401256277200155220ustar00rootroot00000000000000*\~ *#* *.pyc *.egg-info/ *.egg *.mypy_cache/ .coverage coverage.xml /.tox/ /build/ /dist/ /env/ /.eggs/ .vscode python-hl7-0.4.2/.hgignore000066400000000000000000000000641401256277200153370ustar00rootroot00000000000000syntax: glob *.pyc *.egg-info *\#* *~* build/ dist/ python-hl7-0.4.2/AUTHORS000066400000000000000000000003361401256277200146060ustar00rootroot00000000000000* `John Paulett `_ (john -at- paulett.org) * `Andrew Wason `_ * `Kevin Gill `_ * `Emilien Klein `_ python-hl7-0.4.2/LICENSE000066400000000000000000000026341401256277200145460ustar00rootroot00000000000000Copyright (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.in000066400000000000000000000001601401256277200152670ustar00rootroot00000000000000include LICENSE README.rst include *.py include tests/*.py include docs/** exclude docs/_build global-exclude *~python-hl7-0.4.2/Makefile000066400000000000000000000023211401256277200151720ustar00rootroot00000000000000.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.rst000066400000000000000000000011611401256277200152220ustar00rootroot00000000000000python-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/000077500000000000000000000000001401256277200144645ustar00rootroot00000000000000python-hl7-0.4.2/docs/.gitignore000066400000000000000000000000111401256277200164440ustar00rootroot00000000000000/_build/ python-hl7-0.4.2/docs/Makefile000066400000000000000000000107761401256277200161370ustar00rootroot00000000000000# 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/000077500000000000000000000000001401256277200161125ustar00rootroot00000000000000python-hl7-0.4.2/docs/_static/.gitignore000066400000000000000000000000001401256277200200700ustar00rootroot00000000000000python-hl7-0.4.2/docs/_templates/000077500000000000000000000000001401256277200166215ustar00rootroot00000000000000python-hl7-0.4.2/docs/_templates/.gitignore000066400000000000000000000000001401256277200205770ustar00rootroot00000000000000python-hl7-0.4.2/docs/accessors.rst000066400000000000000000000213411401256277200172040ustar00rootroot00000000000000Message 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.rst000066400000000000000000000047761401256277200160050ustar00rootroot00000000000000python-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.rst000066400000000000000000000000521401256277200167000ustar00rootroot00000000000000Authors ======= .. include:: ../AUTHORS python-hl7-0.4.2/docs/changelog.rst000066400000000000000000000132621401256277200171510ustar00rootroot00000000000000Changelog ========= 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.py000066400000000000000000000207231401256277200157670ustar00rootroot00000000000000# -*- 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.rst000066400000000000000000000025251401256277200174000ustar00rootroot00000000000000Contributing ============ 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.rst000066400000000000000000000214211401256277200163250ustar00rootroot00000000000000python-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.rst000066400000000000000000000000701401256277200166350ustar00rootroot00000000000000License ======= .. include:: ../LICENSE :literal: python-hl7-0.4.2/docs/make.bat000066400000000000000000000106471401256277200161010ustar00rootroot00000000000000@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.rst000066400000000000000000000102011401256277200161540ustar00rootroot00000000000000MLLP 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.rst000066400000000000000000000036261401256277200172020ustar00rootroot00000000000000.. _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/000077500000000000000000000000001401256277200142265ustar00rootroot00000000000000python-hl7-0.4.2/hl7/__init__.py000066400000000000000000000030561401256277200163430ustar00rootroot00000000000000# -*- 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.py000066400000000000000000000056061401256277200164110ustar00rootroot00000000000000# -*- 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.py000066400000000000000000000165601401256277200160660ustar00rootroot00000000000000import 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.py000066400000000000000000000743651401256277200167640ustar00rootroot00000000000000# -*- 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.py000066400000000000000000000042451401256277200166030ustar00rootroot00000000000000# -*- 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.py000066400000000000000000000004001401256277200167530ustar00rootroot00000000000000class 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/000077500000000000000000000000001401256277200151725ustar00rootroot00000000000000python-hl7-0.4.2/hl7/mllp/__init__.py000066400000000000000000000006611401256277200173060ustar00rootroot00000000000000from .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.py000066400000000000000000000001451401256277200177250ustar00rootroot00000000000000class InvalidBlockError(Exception): """An MLLP Block was received that violates MLLP protocol""" python-hl7-0.4.2/hl7/mllp/streams.py000066400000000000000000000254301401256277200172260ustar00rootroot00000000000000import 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.py000066400000000000000000000360141401256277200161000ustar00rootroot00000000000000# -*- 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.py000066400000000000000000000043141401256277200155570ustar00rootroot00000000000000# -*- 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.py000066400000000000000000000013331401256277200162650ustar00rootroot00000000000000# -*- 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.txt000066400000000000000000000002751401256277200170240ustar00rootroot00000000000000# 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.cfg000066400000000000000000000005551401256277200153620ustar00rootroot00000000000000[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.py000066400000000000000000000032311401256277200152450ustar00rootroot00000000000000# -*- 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/000077500000000000000000000000001401256277200146765ustar00rootroot00000000000000python-hl7-0.4.2/tests/__init__.py000066400000000000000000000000001401256277200167750ustar00rootroot00000000000000python-hl7-0.4.2/tests/backports/000077500000000000000000000000001401256277200166665ustar00rootroot00000000000000python-hl7-0.4.2/tests/backports/__init__.py000066400000000000000000000000001401256277200207650ustar00rootroot00000000000000python-hl7-0.4.2/tests/backports/unittest/000077500000000000000000000000001401256277200205455ustar00rootroot00000000000000python-hl7-0.4.2/tests/backports/unittest/LICENSE000066400000000000000000000332521401256277200215570ustar00rootroot00000000000000# 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__.py000066400000000000000000000000001401256277200226440ustar00rootroot00000000000000python-hl7-0.4.2/tests/backports/unittest/async_case.py000066400000000000000000000141241401256277200232310ustar00rootroot00000000000000# 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.py000066400000000000000000001651711401256277200220450ustar00rootroot00000000000000# 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.py000066400000000000000000000303601401256277200167160ustar00rootroot00000000000000# -*- 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.py000066400000000000000000000014031401256277200201070ustar00rootroot00000000000000# -*- 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.py000066400000000000000000000201571401256277200175720ustar00rootroot00000000000000import 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.py000066400000000000000000000034531401256277200210460ustar00rootroot00000000000000# -*- 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.py000066400000000000000000000110141401256277200204510ustar00rootroot00000000000000# -*- 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.py000066400000000000000000000027621401256277200201120ustar00rootroot00000000000000from 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.py000066400000000000000000000054401401256277200172560ustar00rootroot00000000000000import 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.py000066400000000000000000000414661401256277200174340ustar00rootroot00000000000000# -*- 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.py000066400000000000000000000036061401256277200172710ustar00rootroot00000000000000# -*- 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.py000066400000000000000000000024671401256277200200050ustar00rootroot00000000000000from 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.ini000066400000000000000000000006701401256277200150520ustar00rootroot00000000000000[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