hl7-0.3.4/0000775000175000017500000000000012734262336013724 5ustar jpaulettjpaulett00000000000000hl7-0.3.4/PKG-INFO0000664000175000017500000000214112734262336015017 0ustar jpaulettjpaulett00000000000000Metadata-Version: 1.1 Name: hl7 Version: 0.3.4 Summary: Python library parsing HL7 v2.x messages Home-page: http://python-hl7.readthedocs.org Author: John Paulett Author-email: john -at- paulett.org License: BSD 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 Keywords: HL7,Health Level 7,healthcare,health care,medical record Platform: POSIX Platform: Windows Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Development Status :: 3 - Alpha Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Healthcare Industry Classifier: Topic :: Communications Classifier: Topic :: Scientific/Engineering :: Medical Science Apps. Classifier: Topic :: Software Development :: Libraries :: Python Modules hl7-0.3.4/docs/0000775000175000017500000000000012734262336014654 5ustar jpaulettjpaulett00000000000000hl7-0.3.4/docs/.gitignore0000664000175000017500000000001112722072430016623 0ustar jpaulettjpaulett00000000000000/_build/ hl7-0.3.4/docs/changelog.rst0000664000175000017500000001037112734261751017337 0ustar jpaulettjpaulett00000000000000Change Log ========== 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 hl7-0.3.4/docs/Makefile0000664000175000017500000001077612722072430016316 0ustar jpaulettjpaulett00000000000000# 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." hl7-0.3.4/docs/accessors.rst0000664000175000017500000002147412722072430017372 0ustar jpaulettjpaulett00000000000000python-hl7 - Message Accessor ============================= reproduced from: http://wiki.medical-objects.com.au/index.php/Hl7v2_parsing **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'] u'Field1' >>> h['PID.F2.R1.C1'] u'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'] u'Component1' >>> h[hl7.Accessor('PID', 1, 2, 1, 1)] u'Component1' >>> h.extract_field('PID', 1, 2, 1, 1) u'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'] u'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'] u'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'] u'' 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:] >>> unicode(response) u'MSH|^~\\&|\rMSA' 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' >>> unicode(response) u'MSH|^~\\&|||||||ORU^R01^|||2.4\rMSA|AA||Application Message' 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 'unicode', the escaped value must be returned. .. doctest:: >>> message = 'MSH|^~\&|\r' >>> message += 'PID|Field1|\F\|\r\r' >>> h = hl7.parse(message) >>> unicode(h['PID'][0][2]) u'\\F\\' >>> h.unescape(unicode(h['PID'][0][2])) u'|' When the accessor is used to reference the field, the field is automatically unescaped. .. doctest:: >>> h['PID.F2.R1'] u'|' 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 unicode object. .. doctest:: >>> h.unescape('\\F\\') u'|' >>> h.unescape('\\R\\') u'~' >>> h.unescape('\\S\\') u'^' >>> h.unescape('\\T\\') u'&' >>> h.unescape('\\X202020\\') u' ' >>> h.escape('|~^&') u'\\F\\\\R\\\\S\\\\T\\' >>> h.escape('áéíóú') u'\\Xc3\\\\Xa1\\\\Xc3\\\\Xa9\\\\Xc3\\\\Xad\\\\Xc3\\\\Xb3\\\\Xc3\\\\Xba\\' **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. hl7-0.3.4/docs/index.rst0000664000175000017500000002164512722072430016514 0ustar jpaulettjpaulett00000000000000python-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`. .. 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. .. image:: https://travis-ci.org/johnpaulett/python-hl7.png :target: https://travis-ci.org/johnpaulett/python-hl7 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' 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:: >>> unicode(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] [[u'OBX'], [u'1'], [u'SN'], [[[u'1554-5'], [u'GLUCOSE'], [u'POST 12H CFST:MCNC:PT:SER/PLAS:QN']]], [u''], [[[u''], [u'182']]], [u'mg/dl'], [u'70_105'], [u'H'], [u''], [u''], [u'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] [u'OBX'] >>> h[3][1] [u'1'] >>> h[3][2] [u'SN'] >>> h(4)(2) [u'SN'] We can easily reconstitute this segment as HL7, using the appropriate separators: .. doctest:: >>> unicode(h[3]) u'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] u'GLUCOSE' >>> h[3][3][0][1][0] is h(4)(3)(1)(2)(1) True >>> h[3][5][0][1][0] u'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] u'GLUCOSE' >>> h['OBX'][0][3][0][1][0] u'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] u'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 both Python 2.6+ and Python 3.3+. The library primarily deals in unicode (the ``str`` type in Python 3). Passing a byte string 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. :py:class:`hl7.Message` can be forced back into a string using ``unicode(message)`` in Python 2 and ``str(message)`` in Python 3. :doc:`mllp_send` assumes the stream is already in the correct encoding. :py:class:`hl7.client.MLLPClient`, if given a unicode string or :py:class:`hl7.Message` instance, will use its ``encoding`` method to encode the unicode data to a byte string. Contents -------- .. toctree:: :maxdepth: 1 api mllp_send 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 `_ hl7-0.3.4/docs/contribute.rst0000664000175000017500000000221312722072430017551 0ustar jpaulettjpaulett00000000000000Contributing ============ 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 ... It is also recommended to run the flake8 checks for PEP8 and PyFlake violations. Commits should be free of warnings:: $ make lint hl7-0.3.4/docs/api.rst0000664000175000017500000000322712722072430016152 0ustar jpaulettjpaulett00000000000000python-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' .. autodata:: hl7.NULL .. autofunction:: hl7.parse .. autofunction:: hl7.ishl7 .. 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: __unicode__ .. autoclass:: hl7.Accessor :members: __new__, parse_key, key, _replace, _make, _asdict, segment, segment_num, field_num, repeat_num, component_num, subcomponent_num .. autoclass:: hl7.Message :members: segments, segment, __getitem__, __setitem__, escape, unescape, extract_field, assign_field, 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 hl7-0.3.4/docs/authors.rst0000664000175000017500000000005212722072430017057 0ustar jpaulettjpaulett00000000000000Authors ======= .. include:: ../AUTHORS hl7-0.3.4/docs/conf.py0000664000175000017500000002027212722072430016145 0ustar jpaulettjpaulett00000000000000# -*- 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 # 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' # 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 = 'sphinxdoc' # 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 = {} # 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} hl7-0.3.4/docs/license.rst0000664000175000017500000000007012722072430017014 0ustar jpaulettjpaulett00000000000000License ======= .. include:: ../LICENSE :literal: hl7-0.3.4/docs/mllp_send.rst0000664000175000017500000000362612722072430017361 0ustar jpaulettjpaulett00000000000000.. _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 hl7-0.3.4/docs/make.bat0000664000175000017500000001064712722072430016260 0ustar jpaulettjpaulett00000000000000@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 hl7-0.3.4/MANIFEST.in0000664000175000017500000000016012722072430015446 0ustar jpaulettjpaulett00000000000000include LICENSE README.rst include *.py include tests/*.py include docs/** exclude docs/_build global-exclude *~hl7-0.3.4/hl7.egg-info/0000775000175000017500000000000012734262336016110 5ustar jpaulettjpaulett00000000000000hl7-0.3.4/hl7.egg-info/PKG-INFO0000664000175000017500000000214112734262336017203 0ustar jpaulettjpaulett00000000000000Metadata-Version: 1.1 Name: hl7 Version: 0.3.4 Summary: Python library parsing HL7 v2.x messages Home-page: http://python-hl7.readthedocs.org Author: John Paulett Author-email: john -at- paulett.org License: BSD 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 Keywords: HL7,Health Level 7,healthcare,health care,medical record Platform: POSIX Platform: Windows Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Development Status :: 3 - Alpha Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Healthcare Industry Classifier: Topic :: Communications Classifier: Topic :: Scientific/Engineering :: Medical Science Apps. Classifier: Topic :: Software Development :: Libraries :: Python Modules hl7-0.3.4/hl7.egg-info/dependency_links.txt0000664000175000017500000000000112734262336022156 0ustar jpaulettjpaulett00000000000000 hl7-0.3.4/hl7.egg-info/SOURCES.txt0000664000175000017500000000143612734262336020000 0ustar jpaulettjpaulett00000000000000LICENSE MANIFEST.in README.rst setup.cfg setup.py docs/.gitignore docs/Makefile docs/accessors.rst docs/api.rst docs/authors.rst docs/changelog.rst docs/conf.py docs/contribute.rst docs/index.rst docs/license.rst docs/make.bat docs/mllp_send.rst hl7/__init__.py hl7/accessor.py hl7/client.py hl7/compat.py hl7/containers.py hl7/datatypes.py hl7/parser.py hl7/util.py hl7/version.py hl7.egg-info/PKG-INFO hl7.egg-info/SOURCES.txt hl7.egg-info/dependency_links.txt hl7.egg-info/entry_points.txt hl7.egg-info/requires.txt hl7.egg-info/top_level.txt hl7.egg-info/zip-safe tests/__init__.py tests/compat.py tests/samples.py tests/test_accessor.py tests/test_client.py tests/test_construction.py tests/test_containers.py tests/test_datetime.py tests/test_parse.py tests/test_util.py tests/test_version.pyhl7-0.3.4/hl7.egg-info/zip-safe0000664000175000017500000000000112725356514017542 0ustar jpaulettjpaulett00000000000000 hl7-0.3.4/hl7.egg-info/entry_points.txt0000664000175000017500000000006412734262336021406 0ustar jpaulettjpaulett00000000000000[console_scripts] mllp_send = hl7.client:mllp_send hl7-0.3.4/hl7.egg-info/requires.txt0000664000175000017500000000000312734262336020501 0ustar jpaulettjpaulett00000000000000sixhl7-0.3.4/hl7.egg-info/top_level.txt0000664000175000017500000000000412734262336020634 0ustar jpaulettjpaulett00000000000000hl7 hl7-0.3.4/hl7/0000775000175000017500000000000012734262336014416 5ustar jpaulettjpaulett00000000000000hl7-0.3.4/hl7/accessor.py0000664000175000017500000000507012722072430016563 0ustar jpaulettjpaulett00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals from collections import namedtuple import six from .compat import python_2_unicode_compatible @python_2_unicode_compatible 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 + six.text_type(self.segment_num) return ".".join(six.text_type(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) hl7-0.3.4/hl7/datatypes.py0000664000175000017500000000425112722072430016757 0ustar jpaulettjpaulett00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals import datetime import re import math 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) hl7-0.3.4/hl7/compat.py0000664000175000017500000000110612722072430016240 0ustar jpaulettjpaulett00000000000000"""Python 2/3 Compatibility Helper Inspired by: * https://docs.djangoproject.com/en/dev/topics/python3/ * http://lucumr.pocoo.org/2011/1/22/forwards-compatible-python/ * http://python-future.org/index.html * http://docs.python.org/3.3/howto/pyporting.html """ import six def python_2_unicode_compatible(cls): """ Class decorator that provides appropriate Python 2 __unicode__ and __str__ based upon Python 3' __str__. """ if six.PY2: cls.__unicode__ = cls.__str__ cls.__str__ = lambda self: self.__unicode__().encode('utf-8') return cls hl7-0.3.4/hl7/__init__.py0000664000175000017500000000203712722072430016520 0ustar jpaulettjpaulett00000000000000# -*- 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 .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 = '""' from .parser import parse from .containers import Sequence, Container, Message, Segment, Field, Repetition, Component, Factory from .accessor import Accessor from .util import ishl7, isfile, split_file, generate_message_control_id from .datatypes import parse_datetime __all__ = [ "parse", "Sequence", "Container", "Message", "Segment", "Field", "Repetition", "Component", "Factory", "Accessor", "ishl7", "isfile", "split_file", "generate_message_control_id", "parse_datetime", ] hl7-0.3.4/hl7/parser.py0000664000175000017500000001344212734256010016260 0ustar jpaulettjpaulett00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals import six from .containers import Factory def parse(line, 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(line, six.binary_type): line = line.decode(encoding) # Strip out unnecessary whitespace strmsg = line.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 _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', '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. assert strmsg[:3] in ('MSH') 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 hl7-0.3.4/hl7/containers.py0000664000175000017500000005267612722072430017144 0ustar jpaulettjpaulett00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals import datetime import logging import six from .compat import python_2_unicode_compatible from .accessor import Accessor 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 @python_2_unicode_compatible 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): """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. >>> unicode(h) == message True .. note:: For Python 2.x use ``unicode()``, but for Python 3.x, use ``str()`` """ return self.separator.join((six.text_type(x) for x in self)) class Message(Container): """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 [[u'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 [[[u'OBX'], [u'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, six.string_types): 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("^", [u'PID'], [u''])]) 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, six.string_types) and len(key) > 3 and isinstance(value, six.string_types): 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 [[u'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') [[[u'OBX'], [u'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 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.encode('ascii')) else: rv.append('%sX%2x%s' % (esc, ord(c), esc)) return ''.join(rv) def unescape(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. 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(six.unichr(int(value[off:off + 2], 16))) except: 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(six.text_type(c)) return ''.join(rv) def create_message(self, seq): """Create a new :py:class:`hl7.Message` compatible with this message""" 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 message""" 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 message""" 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 message""" 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 message""" return self.factory.create_component(self.separators[4], seq, esc=self.esc, separators=self.separators[4:], factory=self.factory) 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` (accept), `AR` (reject), `AE` (error) (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(six.text_type(source_msh(1)), 'MSH', 1, 1) ack.assign_field(six.text_type(source_msh(2)), 'MSH', 1, 2) # Sending application is source receving application ack.assign_field(six.text_type(application) if application is not None else six.text_type(source_msh(5)), 'MSH', 1, 3) # Sending facility is source receving facility ack.assign_field(six.text_type(facility) if facility is not None else six.text_type(source_msh(6)), 'MSH', 1, 4) # Receiving application is source sending application ack.assign_field(six.text_type(source_msh(3)), 'MSH', 1, 5) # Receiving facility is source sending facility ack.assign_field(six.text_type(source_msh(4)), 'MSH', 1, 6) ack.assign_field(six.text_type(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(six.text_type(source_msh(9)(1)(2)), 'MSH', 1, 9, 1, 2) ack.assign_field(message_id if message_id is not None else generate_message_control_id(), 'MSH', 1, 10) ack.assign_field(six.text_type(source_msh(11)), 'MSH', 1, 11) ack.assign_field(six.text_type(source_msh(12)), 'MSH', 1, 12) ack.assign_field(six.text_type(ack_code), 'MSA', 1, 1) ack.assign_field(six.text_type(source_msh(10)), 'MSA', 1, 2) return ack @python_2_unicode_compatible 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 six.text_type(self[0]) in ['MSH', 'FHS']: return six.text_type(self[0]) + six.text_type(self[1]) + six.text_type(self[2]) + six.text_type(self[1]) + \ self.separator.join((six.text_type(x) for x in self[3:])) return self.separator.join((six.text_type(x) for x in self)) 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_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` hl7-0.3.4/hl7/version.py0000664000175000017500000000133312734260357016456 0ustar jpaulettjpaulett00000000000000# -*- 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, 3, 4, '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) hl7-0.3.4/hl7/client.py0000664000175000017500000001623712734255375016264 0ustar jpaulettjpaulett00000000000000from optparse import OptionParser import hl7 import os.path import six import socket import sys 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, six.binary_type): # Assume we have the correct encoding binary = message else: # Encode the unicode message into a bytestring if isinstance(message, hl7.Message): message = six.text_type(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 six.PY3 and isinstance(content, six.binary_type): 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 = b'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 (In Py3, we could just call bytes([ints])) separators = [six.byte2int(bs) for bs in [EB, FF, SB]] data = b''.join([six.int2byte(c) for c in six.iterbytes(data) if c 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') return 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 = six.BytesIO(f.read()) else: if options.loose: stderr().write('--loose requires --file\n') return 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() hl7-0.3.4/hl7/util.py0000664000175000017500000000362312722072430015740 0ustar jpaulettjpaulett00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals import string import datetime import random import logging 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] in ['MSH']) or False def isfile(line): """ Files are wrapped in FHS / FTS FHS = file header segment FTS = file trailer segment """ return line and (line.strip()[:3] in ['FHS']) or False 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 hl7-0.3.4/tests/0000775000175000017500000000000012734262336015066 5ustar jpaulettjpaulett00000000000000hl7-0.3.4/tests/samples.py0000664000175000017500000000342412722072430017076 0ustar jpaulettjpaulett00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals # 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_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', '' ]) hl7-0.3.4/tests/test_datetime.py0000664000175000017500000000247112722072430020266 0ustar jpaulettjpaulett00000000000000from __future__ import unicode_literals from hl7.datatypes import parse_datetime, _UTCOffset from .compat import unittest from datetime import datetime class DatetimeTest(unittest.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())) hl7-0.3.4/tests/test_containers.py0000664000175000017500000001036712722072430020642 0ustar jpaulettjpaulett00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals import six import hl7 from hl7 import Segment, Field from .compat import unittest from .samples import sample_hl7 class ContainerTest(unittest.TestCase): def test_unicode(self): msg = hl7.parse(sample_hl7) self.assertEqual(six.text_type(msg), sample_hl7.strip()) self.assertEqual( six.text_type(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(six.text_type(c), '1|b|data') class MessageTest(unittest.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_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.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(unittest.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)) hl7-0.3.4/tests/test_util.py0000664000175000017500000000132312722072430017442 0ustar jpaulettjpaulett00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals import hl7 from .compat import unittest from .samples import sample_hl7, sample_file class IsHL7Test(unittest.TestCase): def test_ishl7(self): self.assertTrue(hl7.ishl7(sample_hl7)) 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_isfile(self): self.assertFalse(hl7.ishl7(sample_file)) self.assertTrue(hl7.isfile(sample_file)) hl7-0.3.4/tests/test_client.py0000664000175000017500000002005112734261052017745 0ustar jpaulettjpaulett00000000000000from hl7.client import MLLPClient, MLLPException, mllp_send, CR, SB, EB from hl7 import __version__ as hl7_version from optparse import Values from shutil import rmtree from tempfile import mkdtemp from .compat import Mock, patch, unittest import hl7 import os import socket class MLLPClientTest(unittest.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('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\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): try: with MLLPClient('localhost', 6666): raise Exception() self.fail() except: # expected pass # socket.close should be called via the with statement self.client.socket.close.assert_called_once_with() class MLLPSendTest(unittest.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() # 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() # 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') 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' ) 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'' hl7-0.3.4/tests/test_parse.py0000664000175000017500000002137412722072430017607 0ustar jpaulettjpaulett00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals import six import hl7 from hl7 import Accessor, Message, Segment, Field, Repetition, Component from .compat import unittest from .samples import sample_hl7, rep_sample_hl7, sample_file class ParseTest(unittest.TestCase): def test_parse(self): msg = hl7.parse(sample_hl7) self.assertEqual(len(msg), 5) self.assertIsInstance(msg[0][0][0], six.text_type) 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], '^~\&') 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(six.text_type(msg), sample_hl7.strip()) self.assertEqual(six.text_type(msg), sample_hl7.strip()) def test_bytestring_converted_to_unicode(self): msg = hl7.parse(six.text_type(sample_hl7)) self.assertEqual(len(msg), 5) self.assertIsInstance(msg[0][0][0], six.text_type) 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], six.text_type) def test_nonstandard_separators(self): nonstd = 'MSH$%~\&$GHH LAB\rPID$$$555-44-4444$$EVERYWOMAN%EVE%E%%%L' msg = hl7.parse(nonstd) self.assertEqual(six.text_type(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(six.text_type(msg[1][4][0][0]), 'Repeat1') self.assertIsInstance(msg[1][4][0][0], six.text_type) self.assertEqual(six.text_type(msg[1][4][1][0]), 'Repeat2') self.assertIsInstance(msg[1][4][1][0], six.text_type) 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.assertRaisesRegexp(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(six.text_type(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\\'), ' ') def test_escape(self): msg = hl7.parse(rep_sample_hl7) 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\\') self.assertEqual(msg.escape('áéíóú'), '\\Xe1\\\\Xe9\\\\Xed\\\\Xf3\\\\Xfa\\') 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(unittest.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) hl7-0.3.4/tests/test_accessor.py0000664000175000017500000000143412722072430020272 0ustar jpaulettjpaulett00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals from hl7 import Accessor from .compat import unittest class AccessorTest(unittest.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)) hl7-0.3.4/tests/compat.py0000664000175000017500000000046712722072430016721 0ustar jpaulettjpaulett00000000000000import sys # TODO Remove once 2.6 compat is removed if sys.version_info < (3, 0): import unittest2 as unittest else: import unittest try: # Added in Python 3.3 from unittest.mock import patch, Mock except ImportError: from mock import patch, Mock __all__ = ['unittest', 'patch', 'Mock'] hl7-0.3.4/tests/__init__.py0000664000175000017500000000000012722072430017154 0ustar jpaulettjpaulett00000000000000hl7-0.3.4/tests/test_construction.py0000664000175000017500000000351112722072430021220 0ustar jpaulettjpaulett00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals import six from .compat import unittest import hl7 from .samples import rep_sample_hl7 SEP = '|^~\&' CR_SEP = '\r' class ConstructionTest(unittest.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(six.text_type(response), 'MSH|^~\\&|\rMSA') 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(six.text_type(response), 'MSH|^~\\&|\rMSA') 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(six.text_type(response), 'MSH|^~\\&|\rMSA') src_msg = hl7.parse(rep_sample_hl7) PID = src_msg['PID'][0] self.assertEqual(six.text_type(PID), 'PID|Field1|Component1^Component2|Component1^Sub-Component1&Sub-Component2^Component3|Repeat1~Repeat2') response.append(PID) self.assertEqual(six.text_type(response), 'MSH|^~\\&|\rMSA\rPID|Field1|Component1^Component2|Component1^Sub-Component1&Sub-Component2^Component3|Repeat1~Repeat2') hl7-0.3.4/tests/test_version.py0000664000175000017500000000244612722072430020161 0ustar jpaulettjpaulett00000000000000from hl7.version import get_version from .compat import patch, unittest class GetVersionTest(unittest.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()) hl7-0.3.4/README.rst0000664000175000017500000000102312722072430015376 0ustar jpaulettjpaulett00000000000000python-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://travis-ci.org/johnpaulett/python-hl7.png :target: https://travis-ci.org/johnpaulett/python-hl7 .. warning:: python-hl7 v0.3.0 breaks `backwards compatibility `_. hl7-0.3.4/LICENSE0000664000175000017500000000263412722072430014725 0ustar jpaulettjpaulett00000000000000Copyright (C) 2009-2011 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. hl7-0.3.4/setup.cfg0000644000175000017500000000017612734262336015547 0ustar jpaulettjpaulett00000000000000[flake8] ignore = E501 exclude = ._* [bdist_wheel] universal = 1 [egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 hl7-0.3.4/setup.py0000775000175000017500000000357312734260357015452 0ustar jpaulettjpaulett00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- from setuptools import setup import sys # 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()) tests_require = [] if sys.version_info < (3, 0): tests_require.extend([ 'unittest2>=0.5.1' ]) if sys.version_info < (3, 3): tests_require.extend([ 'mock==1.0.1' ]) 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 -at- paulett.org', url='http://python-hl7.readthedocs.org', license='BSD', platforms=['POSIX', 'Windows'], keywords=[ 'HL7', 'Health Level 7', 'healthcare', 'health care', 'medical record' ], 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'], install_requires=['six'], test_suite='tests', tests_require=tests_require, entry_points={ 'console_scripts': [ 'mllp_send=hl7.client:mllp_send', ], }, zip_safe=True, )