├── .coveragerc ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── docs ├── Makefile └── source │ ├── conf.py │ ├── edifact.rst │ └── modules.rst ├── edifact ├── __init__.py ├── configuration.py ├── exceptions.py ├── helpers.py ├── message_headers │ ├── __init__.py │ ├── una.py │ └── unh.py ├── messages │ ├── __init__.py │ ├── base.py │ ├── specs │ │ └── MSCONS.xml │ └── un.py └── segments │ ├── __init__.py │ ├── base.py │ └── un.py ├── pytest.ini ├── requirements.testing.txt ├── requirements.txt ├── setup.cfg ├── setup.py ├── test.py └── tests ├── __init__.py ├── data └── edifact_sample_mscons_1.txt ├── helpers └── __init__.py ├── integration_tests ├── __init__.py └── test_core.py └── unit_tests ├── __init__.py ├── messages ├── __init__.py └── test_base.py └── test_helpers.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = edifact -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | 3 | # Linux 4 | *~ 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | include 15 | lib 16 | local 17 | var 18 | sdist 19 | develop-eggs 20 | .installed.cfg 21 | .eggs 22 | .cache 23 | 24 | # Installer logs 25 | pip-log.txt 26 | 27 | # Unit test / coverage reports 28 | .coverage 29 | .tox 30 | htmlcov 31 | 32 | # Sublime Text 33 | python-edifact.sublime-project 34 | python-edifact.sublime-workspace 35 | 36 | #Translations 37 | *.mo 38 | 39 | #Mr Developer 40 | .mr.developer.cfg 41 | 42 | #Environment 43 | env 44 | 45 | # Virtualenv 46 | venv/ 47 | 48 | violations.flake8.txt 49 | 50 | # Built docs 51 | docs/_build/** 52 | 53 | # Misc 54 | _backup/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | python: 4 | - "2.7" 5 | 6 | install: 7 | - travis_retry pip install . 8 | - pip install -r requirements.testing.txt 9 | - pip install -r requirements.txt 10 | 11 | script: 12 | - py.test --cov 13 | 14 | after_success: 15 | - coveralls 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/FriedrichK/python-edifact.svg?branch=master)](https://travis-ci.org/FriedrichK/python-edifact) 2 | [![Coverage Status](https://coveralls.io/repos/github/FriedrichK/python-edifact/badge.svg?branch=master)](https://coveralls.io/github/FriedrichK/python-edifact?branch=master) 3 | 4 | # python-edifact 5 | An EDIFACT toolkit for Python 6 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 21 | 22 | .PHONY: help 23 | help: 24 | @echo "Please use \`make ' where is one of" 25 | @echo " html to make standalone HTML files" 26 | @echo " dirhtml to make HTML files named index.html in directories" 27 | @echo " singlehtml to make a single large HTML file" 28 | @echo " pickle to make pickle files" 29 | @echo " json to make JSON files" 30 | @echo " htmlhelp to make HTML files and a HTML help project" 31 | @echo " qthelp to make HTML files and a qthelp project" 32 | @echo " applehelp to make an Apple Help Book" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | @echo " coverage to run coverage check of the documentation (if enabled)" 49 | 50 | .PHONY: clean 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | .PHONY: html 55 | html: 56 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 57 | @echo 58 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 59 | 60 | .PHONY: dirhtml 61 | dirhtml: 62 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 63 | @echo 64 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 65 | 66 | .PHONY: singlehtml 67 | singlehtml: 68 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 69 | @echo 70 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 71 | 72 | .PHONY: pickle 73 | pickle: 74 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 75 | @echo 76 | @echo "Build finished; now you can process the pickle files." 77 | 78 | .PHONY: json 79 | json: 80 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 81 | @echo 82 | @echo "Build finished; now you can process the JSON files." 83 | 84 | .PHONY: htmlhelp 85 | htmlhelp: 86 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 87 | @echo 88 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 89 | ".hhp project file in $(BUILDDIR)/htmlhelp." 90 | 91 | .PHONY: qthelp 92 | qthelp: 93 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 94 | @echo 95 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 96 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 97 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/python-edifact.qhcp" 98 | @echo "To view the help file:" 99 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/python-edifact.qhc" 100 | 101 | .PHONY: applehelp 102 | applehelp: 103 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 104 | @echo 105 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 106 | @echo "N.B. You won't be able to view it unless you put it in" \ 107 | "~/Library/Documentation/Help or install it in your application" \ 108 | "bundle." 109 | 110 | .PHONY: devhelp 111 | devhelp: 112 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 113 | @echo 114 | @echo "Build finished." 115 | @echo "To view the help file:" 116 | @echo "# mkdir -p $$HOME/.local/share/devhelp/python-edifact" 117 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/python-edifact" 118 | @echo "# devhelp" 119 | 120 | .PHONY: epub 121 | epub: 122 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 123 | @echo 124 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 125 | 126 | .PHONY: latex 127 | latex: 128 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 129 | @echo 130 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 131 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 132 | "(use \`make latexpdf' here to do that automatically)." 133 | 134 | .PHONY: latexpdf 135 | latexpdf: 136 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 137 | @echo "Running LaTeX files through pdflatex..." 138 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 139 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 140 | 141 | .PHONY: latexpdfja 142 | latexpdfja: 143 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 144 | @echo "Running LaTeX files through platex and dvipdfmx..." 145 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 146 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 147 | 148 | .PHONY: text 149 | text: 150 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 151 | @echo 152 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 153 | 154 | .PHONY: man 155 | man: 156 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 157 | @echo 158 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 159 | 160 | .PHONY: texinfo 161 | texinfo: 162 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 163 | @echo 164 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 165 | @echo "Run \`make' in that directory to run these through makeinfo" \ 166 | "(use \`make info' here to do that automatically)." 167 | 168 | .PHONY: info 169 | info: 170 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 171 | @echo "Running Texinfo files through makeinfo..." 172 | make -C $(BUILDDIR)/texinfo info 173 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 174 | 175 | .PHONY: gettext 176 | gettext: 177 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 178 | @echo 179 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 180 | 181 | .PHONY: changes 182 | changes: 183 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 184 | @echo 185 | @echo "The overview file is in $(BUILDDIR)/changes." 186 | 187 | .PHONY: linkcheck 188 | linkcheck: 189 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 190 | @echo 191 | @echo "Link check complete; look for any errors in the above output " \ 192 | "or in $(BUILDDIR)/linkcheck/output.txt." 193 | 194 | .PHONY: doctest 195 | doctest: 196 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 197 | @echo "Testing of doctests in the sources finished, look at the " \ 198 | "results in $(BUILDDIR)/doctest/output.txt." 199 | 200 | .PHONY: coverage 201 | coverage: 202 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 203 | @echo "Testing of coverage in the sources finished, look at the " \ 204 | "results in $(BUILDDIR)/coverage/python.txt." 205 | 206 | .PHONY: xml 207 | xml: 208 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 209 | @echo 210 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 211 | 212 | .PHONY: pseudoxml 213 | pseudoxml: 214 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 215 | @echo 216 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 217 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # python-edifact documentation build configuration file, created by 4 | # sphinx-quickstart on Sat Mar 26 20:24:56 2016. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | #sys.path.insert(0, os.path.abspath('.')) 22 | SOURCE_PATH = os.path.abspath('../..') 23 | sys.path.insert(0, SOURCE_PATH) 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | #needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | 'sphinx.ext.autodoc', 35 | ] 36 | 37 | # Add any paths that contain templates here, relative to this directory. 38 | templates_path = ['_templates'] 39 | 40 | # The suffix(es) of source filenames. 41 | # You can specify multiple suffix as a list of string: 42 | # source_suffix = ['.rst', '.md'] 43 | source_suffix = '.rst' 44 | 45 | # The encoding of source files. 46 | #source_encoding = 'utf-8-sig' 47 | 48 | # The master toctree document. 49 | master_doc = 'edifact' 50 | 51 | # General information about the project. 52 | project = u'python-edifact' 53 | copyright = u'2016, Friedrich Kauder' 54 | author = u'Friedrich Kauder' 55 | 56 | # The version info for the project you're documenting, acts as replacement for 57 | # |version| and |release|, also used in various other places throughout the 58 | # built documents. 59 | # 60 | # The short X.Y version. 61 | version = u'0.1' 62 | # The full version, including alpha/beta/rc tags. 63 | release = u'0.1' 64 | 65 | # The language for content autogenerated by Sphinx. Refer to documentation 66 | # for a list of supported languages. 67 | # 68 | # This is also used if you do content translation via gettext catalogs. 69 | # Usually you set "language" from the command line for these cases. 70 | language = None 71 | 72 | # There are two options for replacing |today|: either, you set today to some 73 | # non-false value, then it is used: 74 | #today = '' 75 | # Else, today_fmt is used as the format for a strftime call. 76 | #today_fmt = '%B %d, %Y' 77 | 78 | # List of patterns, relative to source directory, that match files and 79 | # directories to ignore when looking for source files. 80 | exclude_patterns = [] 81 | 82 | # The reST default role (used for this markup: `text`) to use for all 83 | # documents. 84 | #default_role = None 85 | 86 | # If true, '()' will be appended to :func: etc. cross-reference text. 87 | #add_function_parentheses = True 88 | 89 | # If true, the current module name will be prepended to all description 90 | # unit titles (such as .. function::). 91 | #add_module_names = True 92 | 93 | # If true, sectionauthor and moduleauthor directives will be shown in the 94 | # output. They are ignored by default. 95 | #show_authors = False 96 | 97 | # The name of the Pygments (syntax highlighting) style to use. 98 | pygments_style = 'sphinx' 99 | 100 | # A list of ignored prefixes for module index sorting. 101 | #modindex_common_prefix = [] 102 | 103 | # If true, keep warnings as "system message" paragraphs in the built documents. 104 | #keep_warnings = False 105 | 106 | # If true, `todo` and `todoList` produce output, else they produce nothing. 107 | todo_include_todos = False 108 | 109 | 110 | # -- Options for HTML output ---------------------------------------------- 111 | 112 | # The theme to use for HTML and HTML Help pages. See the documentation for 113 | # a list of builtin themes. 114 | html_theme = 'default' 115 | 116 | # Theme options are theme-specific and customize the look and feel of a theme 117 | # further. For a list of options available for each theme, see the 118 | # documentation. 119 | #html_theme_options = {} 120 | 121 | # Add any paths that contain custom themes here, relative to this directory. 122 | #html_theme_path = [] 123 | 124 | # The name for this set of Sphinx documents. If None, it defaults to 125 | # " v documentation". 126 | #html_title = None 127 | 128 | # A shorter title for the navigation bar. Default is the same as html_title. 129 | #html_short_title = None 130 | 131 | # The name of an image file (relative to this directory) to place at the top 132 | # of the sidebar. 133 | #html_logo = None 134 | 135 | # The name of an image file (relative to this directory) to use as a favicon of 136 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 137 | # pixels large. 138 | #html_favicon = None 139 | 140 | # Add any paths that contain custom static files (such as style sheets) here, 141 | # relative to this directory. They are copied after the builtin static files, 142 | # so a file named "default.css" will overwrite the builtin "default.css". 143 | html_static_path = ['_static'] 144 | 145 | # Add any extra paths that contain custom files (such as robots.txt or 146 | # .htaccess) here, relative to this directory. These files are copied 147 | # directly to the root of the documentation. 148 | #html_extra_path = [] 149 | 150 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 151 | # using the given strftime format. 152 | #html_last_updated_fmt = '%b %d, %Y' 153 | 154 | # If true, SmartyPants will be used to convert quotes and dashes to 155 | # typographically correct entities. 156 | #html_use_smartypants = True 157 | 158 | # Custom sidebar templates, maps document names to template names. 159 | #html_sidebars = {} 160 | 161 | # Additional templates that should be rendered to pages, maps page names to 162 | # template names. 163 | #html_additional_pages = {} 164 | 165 | # If false, no module index is generated. 166 | #html_domain_indices = True 167 | 168 | # If false, no index is generated. 169 | #html_use_index = True 170 | 171 | # If true, the index is split into individual pages for each letter. 172 | #html_split_index = False 173 | 174 | # If true, links to the reST sources are added to the pages. 175 | #html_show_sourcelink = True 176 | 177 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 178 | #html_show_sphinx = True 179 | 180 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 181 | #html_show_copyright = True 182 | 183 | # If true, an OpenSearch description file will be output, and all pages will 184 | # contain a tag referring to it. The value of this option must be the 185 | # base URL from which the finished HTML is served. 186 | #html_use_opensearch = '' 187 | 188 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 189 | #html_file_suffix = None 190 | 191 | # Language to be used for generating the HTML full-text search index. 192 | # Sphinx supports the following languages: 193 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 194 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' 195 | #html_search_language = 'en' 196 | 197 | # A dictionary with options for the search language support, empty by default. 198 | # Now only 'ja' uses this config value 199 | #html_search_options = {'type': 'default'} 200 | 201 | # The name of a javascript file (relative to the configuration directory) that 202 | # implements a search results scorer. If empty, the default will be used. 203 | #html_search_scorer = 'scorer.js' 204 | 205 | # Output file base name for HTML help builder. 206 | htmlhelp_basename = 'python-edifactdoc' 207 | 208 | # -- Options for LaTeX output --------------------------------------------- 209 | 210 | latex_elements = { 211 | # The paper size ('letterpaper' or 'a4paper'). 212 | #'papersize': 'letterpaper', 213 | 214 | # The font size ('10pt', '11pt' or '12pt'). 215 | #'pointsize': '10pt', 216 | 217 | # Additional stuff for the LaTeX preamble. 218 | #'preamble': '', 219 | 220 | # Latex figure (float) alignment 221 | #'figure_align': 'htbp', 222 | } 223 | 224 | # Grouping the document tree into LaTeX files. List of tuples 225 | # (source start file, target name, title, 226 | # author, documentclass [howto, manual, or own class]). 227 | latex_documents = [ 228 | (master_doc, 'python-edifact.tex', u'python-edifact Documentation', 229 | u'Friedrich Kauder', 'manual'), 230 | ] 231 | 232 | # The name of an image file (relative to this directory) to place at the top of 233 | # the title page. 234 | #latex_logo = None 235 | 236 | # For "manual" documents, if this is true, then toplevel headings are parts, 237 | # not chapters. 238 | #latex_use_parts = False 239 | 240 | # If true, show page references after internal links. 241 | #latex_show_pagerefs = False 242 | 243 | # If true, show URL addresses after external links. 244 | #latex_show_urls = False 245 | 246 | # Documents to append as an appendix to all manuals. 247 | #latex_appendices = [] 248 | 249 | # If false, no module index is generated. 250 | #latex_domain_indices = True 251 | 252 | 253 | # -- Options for manual page output --------------------------------------- 254 | 255 | # One entry per manual page. List of tuples 256 | # (source start file, name, description, authors, manual section). 257 | man_pages = [ 258 | (master_doc, 'python-edifact', u'python-edifact Documentation', 259 | [author], 1) 260 | ] 261 | 262 | # If true, show URL addresses after external links. 263 | #man_show_urls = False 264 | 265 | 266 | # -- Options for Texinfo output ------------------------------------------- 267 | 268 | # Grouping the document tree into Texinfo files. List of tuples 269 | # (source start file, target name, title, author, 270 | # dir menu entry, description, category) 271 | texinfo_documents = [ 272 | (master_doc, 'python-edifact', u'python-edifact Documentation', 273 | author, 'python-edifact', 'One line description of project.', 274 | 'Miscellaneous'), 275 | ] 276 | 277 | # Documents to append as an appendix to all manuals. 278 | #texinfo_appendices = [] 279 | 280 | # If false, no module index is generated. 281 | #texinfo_domain_indices = True 282 | 283 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 284 | #texinfo_show_urls = 'footnote' 285 | 286 | # If true, do not generate a @detailmenu in the "Top" node's menu. 287 | #texinfo_no_detailmenu = False 288 | -------------------------------------------------------------------------------- /docs/source/edifact.rst: -------------------------------------------------------------------------------- 1 | edifact package 2 | =============== 3 | 4 | Submodules 5 | ---------- 6 | 7 | edifact.message module 8 | ---------------------- 9 | 10 | .. automodule:: edifact.message 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | 16 | Module contents 17 | --------------- 18 | 19 | .. automodule:: edifact 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | -------------------------------------------------------------------------------- /docs/source/modules.rst: -------------------------------------------------------------------------------- 1 | edifact 2 | ======= 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | edifact 8 | -------------------------------------------------------------------------------- /edifact/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Provides core functions.""" 3 | 4 | from edifact.configuration import MESSAGE_CLASSES 5 | 6 | from edifact.message_headers.una import UNA 7 | from edifact.message_headers.unh import UNH 8 | 9 | from edifact.messages.un import * 10 | 11 | 12 | def from_string(edifact_string): 13 | """Create a Message from an EDIFACT string.""" 14 | una = extract_una_or_generate_default(edifact_string) 15 | unh = UNH(una=una, src_string=edifact_string) 16 | 17 | message_class = MESSAGE_CLASSES[unh.get('message_type')] 18 | 19 | return message_class(una, src_string=edifact_string) 20 | 21 | 22 | def extract_una_or_generate_default(edifact_string): 23 | """Extract the UNA segment or generate a default one.""" 24 | if edifact_string[:3] == 'UNA': 25 | return UNA(src_string=edifact_string[:9]) 26 | else: 27 | return UNA() 28 | -------------------------------------------------------------------------------- /edifact/configuration.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Library for components to support modularity.""" 3 | 4 | 5 | MESSAGE_CLASSES = {} 6 | SEGMENT_CLASSES = {} 7 | -------------------------------------------------------------------------------- /edifact/exceptions.py: -------------------------------------------------------------------------------- 1 | class InvalidEdifact(ValueError): 2 | """EDIFACT data is invalid.""" 3 | 4 | 5 | class MissingSegmentAtPositionError(InvalidEdifact): 6 | """Segment is mandatory at position but could not be found. Missing or misplaced.""" 7 | -------------------------------------------------------------------------------- /edifact/helpers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Provides helper functions.""" 3 | 4 | import regex 5 | 6 | from edifact.exceptions import MissingSegmentAtPositionError 7 | 8 | 9 | def separate_segments(src_string, segment_terminator='\'', release_character='?'): 10 | """Separate the segments in an EDIFACT message string.""" 11 | separator_pattern = r'(?= len(components): 126 | if mandatory is True: 127 | raise ValueError('UNH header is missing mandatory entry for {label}'.format(label=label)) 128 | else: 129 | break 130 | 131 | # Process 132 | if len(value) == LENGTH_OF_A_SIMPLE_ENTRY: 133 | data[label] = components[index] 134 | elif len(value) == LENGTH_OF_A_NESTED_ENTRY: 135 | data[label] = process_subentries(components, index) 136 | else: 137 | raise ValueError('unexpected structure') 138 | 139 | return data 140 | 141 | 142 | def process_subentries(components, parent_index): 143 | """Process sub-entries.""" 144 | subentries_data = {} 145 | 146 | parent_label = STRUCTURE[parent_index][0] 147 | number_of_mandatory_subentries = STRUCTURE[parent_index][2] 148 | 149 | subentry_structure = STRUCTURE[parent_index][3] 150 | subentries = components[parent_index] 151 | 152 | if len(subentries) < number_of_mandatory_subentries: 153 | raise ValueError('entry for {0} has {1} mandatory subentries: {2} found'.format(parent_label, number_of_mandatory_subentries, len(subentries))) 154 | 155 | for index, value in enumerate(subentry_structure): 156 | if index >= len(subentries): 157 | break 158 | 159 | label = value[0] 160 | subentries_data[label] = subentries[index] 161 | 162 | return subentries_data 163 | -------------------------------------------------------------------------------- /edifact/messages/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FriedrichK/python-edifact/77a7cf3d053fb4dbb21e61559d603ab609f96e19/edifact/messages/__init__.py -------------------------------------------------------------------------------- /edifact/messages/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """This module provides the base for message wrappers.""" 3 | 4 | import os 5 | 6 | import six 7 | 8 | import xml.etree.ElementTree as ET 9 | import random 10 | import string 11 | 12 | from edifact.helpers import separate_segments, separate_components, validate_anchor_segments 13 | from edifact.exceptions import MissingSegmentAtPositionError 14 | # from edifact.configuration import SEGMENT_CLASSES 15 | 16 | import logging 17 | edifact_logger = logging.getLogger('edifact') 18 | 19 | 20 | class MessageMeta(type): 21 | """Meta class for segments.""" 22 | 23 | def __new__(cls, name, bases, attrs): 24 | """Create class.""" 25 | if 'Meta' in attrs: 26 | attrs = generate_structure(attrs) 27 | return super(MessageMeta, cls).__new__(cls, name, bases, attrs) 28 | 29 | 30 | def generate_structure(attrs): 31 | xml_spec = get_xml_spec(attrs['Meta']) 32 | 33 | attrs['elements'] = [] 34 | 35 | for child in xml_spec.getroot(): 36 | attrs['elements'].append(process_element(child)) 37 | 38 | return attrs 39 | 40 | 41 | def process_element(element): 42 | if element.tag == 'GROUP': 43 | return process_group(element) 44 | else: 45 | return PlaceholderSegment(element.tag, **element.attrib) 46 | 47 | 48 | def process_group(element): 49 | elements = [] 50 | for child in element: 51 | elements.append(process_element(child)) 52 | return SegmentGroup(elements, **element.attrib) 53 | 54 | 55 | def get_xml_spec(meta): 56 | spec_dir = meta.spec_dir if hasattr(meta, 'spec_dir') else os.path.dirname(__file__) + '/specs' 57 | return ET.parse('{spec_dir}/{spec}.xml'.format(spec_dir=spec_dir, spec=meta.spec)) 58 | 59 | 60 | class Message(six.with_metaclass(MessageMeta)): 61 | """Base class for segments.""" 62 | 63 | total_number_of_segments = 31 64 | 65 | data = {} 66 | 67 | def __init__(self, una, src_string=None): 68 | """Constructor.""" 69 | if src_string: 70 | self.initialize_from_src_string(src_string) 71 | 72 | def initialize_from_src_string(self, src_string): 73 | segments = [separate_components(segment) for segment in separate_segments(src_string)] 74 | validate_anchor_segments(segments) 75 | self.process_segments(segments, 0, [0], 0) 76 | 77 | def process_segments(self, segments, segment_index, elements_indices, repeats, last_containers=[]): 78 | edifact_logger.debug('------------------------- process segment (%s/%s) ----------------------' % (segment_index, len(segments),)) 79 | edifact_logger.debug('element indices: ' + ', '.join([unicode(ei) for ei in elements_indices]) + ', repeats: %s' % repeats) 80 | 81 | # Exit at the end of segments or elements 82 | if segment_index >= len(segments) or len(elements_indices) < 1: 83 | edifact_logger.debug('end of segments') 84 | return 85 | 86 | # Skip certain segments 87 | segments_to_ignore = ['UNA', 'UNH', 'BGM', 'UNT'] 88 | tag = segments[segment_index][0] 89 | if tag in segments_to_ignore: 90 | edifact_logger.debug('ignoring segment %s with tag %s' % (segment_index, tag,)) 91 | self.process_segments( 92 | segments, segment_index + 1, elements_indices, repeats, last_containers 93 | ) 94 | return 95 | 96 | # Process segment 97 | segment_or_group = self.get_element(elements_indices) 98 | edifact_logger.debug('segment %s -> %s' % (segment_index, segments[segment_index],)) 99 | 100 | # End of group or hierarchy 101 | if segment_or_group is None: 102 | edifact_logger.debug('element is None') 103 | segment_index, elements_indices, repeats, last_containers = self.process_end( 104 | segments, segment_index, elements_indices, repeats, last_containers 105 | ) 106 | 107 | # Process group 108 | if isinstance(segment_or_group, SegmentGroup): 109 | edifact_logger.debug('element is a group -> %s' % segment_or_group.label) 110 | segment_index, elements_indices, repeats, last_containers = self.process_group( 111 | segments, segment_index, elements_indices, repeats, last_containers 112 | ) 113 | 114 | # Process segment 115 | if isinstance(segment_or_group, PlaceholderSegment): 116 | edifact_logger.debug('element is a segment') 117 | segment_index, elements_indices, repeats, last_containers = self.process_segment( 118 | segments, segment_index, elements_indices, repeats, last_containers 119 | ) 120 | 121 | # Proceed 122 | self.process_segments(segments, segment_index, elements_indices, repeats, last_containers) 123 | 124 | def process_end(self, segments, segment_index, elements_indices, repeats, last_containers): 125 | # Move one level up 126 | elements_indices = elements_indices[:-1] 127 | 128 | # Reset a possible repeat cycle 129 | repeats = 0 130 | 131 | # Void last container, unless empty 132 | if len(last_containers) > 0: 133 | del last_containers[-1] 134 | edifact_logger.debug('contracted groups %s' % ', '.join([c.label for c in last_containers])) 135 | 136 | # Return 137 | return segment_index, elements_indices, repeats, last_containers 138 | 139 | def process_group(self, segments, segment_index, elements_indices, repeats, last_containers): 140 | tag = segments[segment_index][0] 141 | group = self.get_element(elements_indices) 142 | 143 | # If repeats are exhausted for this group, we stop right there 144 | if repeats + 1 >= group.repeats: 145 | return self.process_end(segments, segment_index, elements_indices, repeats, last_containers) 146 | else: 147 | repeats += 1 148 | 149 | # Group does not have current segment as first segment 150 | if not group_starts_with_segment(group, tag): 151 | 152 | # Crash is group is mandatory and this is not a repeat attempt 153 | if group.mandatory and repeats == 0: 154 | raise MissingSegmentAtPositionError(group.get(0).tag) 155 | 156 | # Otherwise move on 157 | else: 158 | repeats = 0 159 | elements_indices[-1] += 1 160 | 161 | # Create and enter group 162 | else: 163 | last_containers.append(self.add_group(group, last_containers, repeats)) 164 | edifact_logger.debug('expanded groups %s' % ', '.join([c.label for c in last_containers])) 165 | elements_indices.append(0) 166 | 167 | # Return 168 | return segment_index, elements_indices, repeats, last_containers 169 | 170 | def process_segment(self, segments, segment_index, elements_indices, repeats, last_containers): 171 | tag = segments[segment_index][0] 172 | element = self.get_element(elements_indices) 173 | 174 | # Tags don't match 175 | if not element.tag == tag: 176 | 177 | # Crash if segment is mandatory and this is not a repeat attempt 178 | if element.mandatory and repeats == 0: 179 | raise MissingSegmentAtPositionError(element.tag) 180 | 181 | # Otherwise move on 182 | else: 183 | repeats = 0 184 | elements_indices[-1] += 1 185 | 186 | # Process matching tags 187 | else: 188 | # Add segment to instance 189 | self.add_segment(element, segments[segment_index], last_containers) 190 | 191 | # Move to next segment 192 | segment_index += 1 193 | 194 | # Move on to next element if repeats are exhausted, otherwise repeat 195 | edifact_logger.debug('check repeats: %s' % repeats) 196 | if repeats + 1 >= element.repeats: 197 | elements_indices[-1] += 1 198 | repeats = 0 199 | else: 200 | repeats += 1 201 | 202 | # Return 203 | return segment_index, elements_indices, repeats, last_containers 204 | 205 | def add_group(self, placeholder_group, parent_groups, repeats): 206 | group = Group(**placeholder_group.__dict__()) 207 | edifact_logger.debug('created new group %s' % group.label) 208 | 209 | if parent_groups is not None and len(parent_groups) > 0: 210 | parent_groups[-1].add(group.label, group) 211 | edifact_logger.debug('added group %s with uid %s to parent group %s' % (group.label, group.uid, parent_groups[-1].label,)) 212 | else: 213 | self.add_to_data(group.label, group) 214 | edifact_logger.debug('added group %s with uid %s to root' % (group.label, group.uid,)) 215 | 216 | return group 217 | 218 | def add_segment(self, element, segment_data, parent_groups): 219 | segment = DummySegment(segment_data[1:], **element.__dict__()) 220 | edifact_logger.debug('new segment with data: %s' % segment_data) 221 | 222 | if parent_groups is not None and len(parent_groups) > 0: 223 | parent_groups[-1].add(segment.label, segment) 224 | edifact_logger.debug('added segment %s to group %s with uid %s' % (segment.label, parent_groups[-1].label, parent_groups[-1].uid,)) 225 | else: 226 | self.add_to_data(segment.label, segment) 227 | edifact_logger.debug('added segment %s to root' % segment.label) 228 | 229 | def get_element(self, elements_indices): 230 | if elements_indices[0] >= len(self.elements): 231 | return None 232 | 233 | result = self.elements[elements_indices[0]] 234 | for idx in elements_indices[1:]: 235 | try: 236 | result = result.get(idx) 237 | except IndexError: 238 | result = None 239 | return result 240 | 241 | def add_to_data(self, label, element): 242 | if label not in self.data: 243 | self.data[label] = [] 244 | self.data[label].append(element) 245 | 246 | # Magic 247 | def __getitem__(self, key): 248 | return self.data[key] 249 | 250 | 251 | def group_starts_with_segment(group, tag): 252 | return group.get(0).tag == tag 253 | 254 | 255 | # Just for now 256 | class PlaceholderSegment(object): 257 | def __init__(self, tag, status='M', repeats='1', label=None, description=None): 258 | self.tag = tag 259 | self.mandatory = status == 'M' 260 | self.repeats = int(repeats) 261 | self.label = label 262 | self.description = description 263 | 264 | def __dict__(self): 265 | return { 266 | 'tag': self.tag, 267 | 'label': self.label, 268 | 'description': self.description 269 | } 270 | 271 | 272 | class SegmentGroup(object): 273 | def __init__(self, elements, status='M', repeats='1', label=None, description=None): 274 | self.elements = elements 275 | self.mandatory = status == 'M' 276 | self.repeats = int(repeats) 277 | self.label = label 278 | self.description = description 279 | 280 | def get(self, index): 281 | return self.elements[index] 282 | 283 | def __dict__(self): 284 | return { 285 | 'mandatory': self.mandatory, 286 | 'repeats': self.repeats, 287 | 'label': self.label, 288 | 'description': self.description 289 | } 290 | 291 | 292 | class Group(object): 293 | def __init__(self, mandatory=False, repeats=1, label=None, description=None, repeat=0): 294 | self.mandatory = mandatory 295 | self.repeats = repeats 296 | self.label = label 297 | self.description = description 298 | self.repeat = repeat 299 | self.uid = ''.join(random.choice(string.lowercase) for i in range(5)) 300 | 301 | def add(self, label, element): 302 | if not hasattr(self, 'elements'): 303 | self.elements = {} 304 | if label not in self.elements: 305 | self.elements[label] = [] 306 | self.elements[label].append(element) 307 | edifact_logger.debug('group %s (%s) added element %s, elements are now %s' % (self.label, self.uid, element.label, len(self.elements),)) 308 | 309 | 310 | # Mock 311 | class DummySegment(object): 312 | def __init__(self, data, tag=None, label=None, description=None): 313 | self.data = data 314 | self.tag = tag 315 | self.label = label if label is not None else tag 316 | self.description = description 317 | -------------------------------------------------------------------------------- /edifact/messages/specs/MSCONS.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /edifact/messages/un.py: -------------------------------------------------------------------------------- 1 | import edifact.configuration 2 | from edifact.messages.base import Message 3 | 4 | 5 | class MSCONS(Message): 6 | class Meta: 7 | spec = 'MSCONS' 8 | 9 | edifact.configuration.MESSAGE_CLASSES['MSCONS'] = MSCONS 10 | -------------------------------------------------------------------------------- /edifact/segments/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FriedrichK/python-edifact/77a7cf3d053fb4dbb21e61559d603ab609f96e19/edifact/segments/__init__.py -------------------------------------------------------------------------------- /edifact/segments/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """This module provides the base for segment wrappers.""" 3 | 4 | import six 5 | 6 | 7 | class Composite(object): 8 | """Part of a segment.""" 9 | 10 | _content = None 11 | 12 | def __init__(self, index=0, max_length=3, required=False): 13 | """Constructor.""" 14 | self.index = index 15 | self.max_length = max_length 16 | self.required = required 17 | 18 | @property 19 | def content(self): 20 | """Get value.""" 21 | return self._content 22 | 23 | @content.setter 24 | def content(self, content): 25 | """Set content.""" 26 | if len(content) > self.max_length: 27 | raise ValueError('trying to set content {0} for composite with maximum length {1}'.format(content, unicode(self.max_length))) 28 | self._content = content 29 | 30 | def __str__(self): 31 | """Return value.""" 32 | return self.content or u'' 33 | 34 | 35 | class SegmentMeta(type): 36 | """Meta class for segments.""" 37 | 38 | def __new__(cls, name, bases, attrs): 39 | """Create class.""" 40 | cleanup = [] 41 | 42 | # composites 43 | composites = {} 44 | for key, value in attrs.iteritems(): 45 | if isinstance(value, Composite): 46 | composites[key] = value 47 | cleanup.append(key) 48 | attrs['_composites'] = composites 49 | 50 | # cleanup 51 | for key in cleanup: 52 | del attrs[key] 53 | 54 | # Meta 55 | attrs['_meta'] = attrs.pop('Meta', None) 56 | 57 | return super(SegmentMeta, cls).__new__(cls, name, bases, attrs) 58 | 59 | 60 | class Segment(six.with_metaclass(SegmentMeta)): 61 | """Base class for segments.""" 62 | 63 | def __init__(self, una, **kwargs): 64 | """Constructor.""" 65 | self.una = una 66 | 67 | for key, value in kwargs.iteritems(): 68 | if key not in self._composites: 69 | raise IndexError('composite {0} not found'.format(key,)) 70 | self._composites[key].content = value 71 | 72 | def __str__(self): 73 | """Return the string representation of this segment.""" 74 | ordered_composites = [unicode(composite) for composite in sorted(self._composites.values(), key=lambda x: x.index)] 75 | return ''.join(( 76 | self._meta.identifier, # segment tag 77 | self.una.data_element_separator, # segment tag separator 78 | self.una.component_data_element_separator.join(ordered_composites), # composites 79 | self.una.segment_terminator, # terminator 80 | )) 81 | -------------------------------------------------------------------------------- /edifact/segments/un.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """This module provides wrappers for different segment types.""" 3 | 4 | 5 | from edifact.segments.base import Composite, Segment 6 | 7 | 8 | class DTM(Segment): 9 | """Date/Time/Period segment.""" 10 | 11 | qualifier = Composite(index=0, max_length=3, required=True) 12 | value = Composite(index=1, max_length=35, required=True) 13 | format = Composite(index=2, max_length=3, required=True) 14 | 15 | class Meta: 16 | identifier = 'DTM' 17 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | norecursedirs = venv -------------------------------------------------------------------------------- /requirements.testing.txt: -------------------------------------------------------------------------------- 1 | pytest>=2.8.7 2 | pytest-cov>=2.2.0 3 | pytest-runner>=2.6.2 4 | pytest-capturelog>=0.7 5 | mccabe>=0.3.1 6 | flake8>=2.5.2 7 | mock>=1.3.0 8 | pylint>=1.5.5 9 | coverage>=4.0.3 10 | coveralls>=1.1 11 | codecov>=1.6.3 12 | check-manifest>=0.30 13 | Sphinx>=1.3.6 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | six>=1.10.0 2 | regex==2016.03.26 -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | test = pytest 3 | 4 | [flake8] 5 | ignore = D101,D102,D103,D105 6 | max-line-length = 160 7 | exclude = venv,tests 8 | 9 | [pytest] 10 | addopts = --cov 11 | 12 | [coverage:run] 13 | source = edifact -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup, find_packages 4 | 5 | 6 | setup( 7 | name='python-edifact', 8 | version='0.1', 9 | description='EDIFACT toolkit for Python', 10 | author='Friedrich Kauder', 11 | author_email='fkauder@gmail.com', 12 | url='https://github.com/FriedrichK/python-edifact', 13 | packages=find_packages(exclude=('tests', 'docs')), 14 | setup_requires=['pytest-runner'], 15 | tests_require=['pytest'], 16 | ) 17 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | 4 | if __name__ == '__main__': 5 | testsuite = unittest.TestLoader().discover('.') 6 | unittest.TextTestRunner(verbosity=1).run(testsuite) 7 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FriedrichK/python-edifact/77a7cf3d053fb4dbb21e61559d603ab609f96e19/tests/__init__.py -------------------------------------------------------------------------------- /tests/data/edifact_sample_mscons_1.txt: -------------------------------------------------------------------------------- 1 | # UNH+1+MSCONS:D:01B:UN:EAN004' Message header 2 | # BGM+94E::9+6078+9' Metered services consumption report number 6078 3 | # DTM+137:20020204:102' Message date 4th February 2002 4 | # NAD+SU+5071615111110::9' Supplier identified by GTIN 5071615111110 5 | # NAD+BY+5098765111111::9' Buyer identified by GLN 5098765111111 6 | # UNS+D' Separation between the header section and the detail section 7 | # NAD+DP+5098765222220::9' Delivery party identified by GLN 5098765222220 8 | # LOC+17E+CC-5523-4061::91' Location of the meter using supplier internal code CC-5523-4061 9 | # DTM+263:2002010120020131:718' Invoicing period 1st January 2002 to 31st January 2002 10 | # RFF+IV:AX-3255' Invoice reference number AX-3255' 11 | # DTM+171:20020204:102' Date of the invoice referenced 4th February 2002 12 | # LIN+1++4000862141404:SRV' Line item number 1 identified with GTIN 4000862141404 13 | # PRI+INF:25:CT:NTP:1:LTR' Information price from contract 25 per Litre 14 | # NAD+SU+++ESSO IXELLES’ Supplier Address ESSO Ixelles' 15 | # MOA+203:1000' Line item amount 1.000 16 | # QTY+47:40' Invoiced quantity 40 17 | # DTM+6:200201141015:718' Sales date 14th January at 10.15 18 | # LIN+2++4000862141404:SRV' Line item number 2 identified with GTIN 4000862141404 19 | # PRI+INF:25:CT:NTP:1:LTR' Information price from contract 25 per Litre 20 | # NAD+SU+++ESSO ST GILLES' Supplier address ESSO St Gilles' 21 | # MOA+203:1300' Line item amount 1.300 22 | # QTY+47:52' Invoiced quantity 52 23 | # DTM+6:200201261645:718' Sales date 26th January at 16.45 24 | # LIN+3++5412345111184:SRV' Line item number 3 identified with GTIN 5412345111184 25 | # PRI+INF:36:CT:NTP:1:LTR' Information price from contract 36 per Litre 26 | # NAD+SU+++ESSO ST GILLES' Supplier address ESSO St Gilles' 27 | # MOA+203:144' Line item amount 144 28 | # QTY+47:4' Invoiced quantity 4 29 | # DTM+6:200201261645:718' Sales date 26th January at 16.45 30 | # CNT+31E:1' Number of premises 1 31 | # CNT+36E:1' Number of meters 1 32 | # UNT+32+1' Total number of segments in the message equals 32 33 | UNH+1+MSCONS:D:01B:UN:EAN004'BGM+94E::9+6078+9'DTM+137:20020204:102'NAD+SU+5071615111110::9'NAD+BY+5098765111111::9'UNS+D'NAD+DP+5098765222220::9'LOC+17E+CC-5523-4061::91'DTM+263:2002010120020131:718'RFF+IV:AX-3255'DTM+171:20020204:102'LIN+1++4000862141404:SRV'PRI+INF:25:CT:NTP:1:LTR'NAD+SU+++ESSO IXELLES’'MOA+203:1000'QTY+47:40'DTM+6:200201141015:718'LIN+2++4000862141404:SRV'PRI+INF:25:CT:NTP:1:LTR'NAD+SU+++ESSO ST GILLES'MOA+203:1300'QTY+47:52'DTM+6:200201261645:718'LIN+3++5412345111184:SRV'PRI+INF:36:CT:NTP:1:LTR'NAD+SU+++ESSO ST GILLES'MOA+203:144'QTY+47:4'DTM+6:200201261645:718'CNT+31E:1'CNT+36E:1'UNT+32+1' -------------------------------------------------------------------------------- /tests/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Helper functions for tests.""" 3 | 4 | import os 5 | 6 | 7 | def load_edifact_sample(name): 8 | """Load an EDIFACT text sample, ignoring commented lines.""" 9 | path = os.path.dirname(__file__) + '/../data/' + name 10 | content = "" 11 | 12 | for line in open(path): 13 | stripped_line = line.strip() 14 | if not stripped_line.startswith("#"): 15 | content += line 16 | 17 | return content 18 | -------------------------------------------------------------------------------- /tests/integration_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FriedrichK/python-edifact/77a7cf3d053fb4dbb21e61559d603ab609f96e19/tests/integration_tests/__init__.py -------------------------------------------------------------------------------- /tests/integration_tests/test_core.py: -------------------------------------------------------------------------------- 1 | """Tests the core functionality.""" 2 | 3 | import unittest 4 | 5 | from tests.helpers import load_edifact_sample 6 | 7 | from edifact import from_string 8 | 9 | 10 | class ParseTest(unittest.TestCase): 11 | """Test the parse functionality.""" 12 | 13 | def test_returns_expected_segment_count(self): 14 | mscons_sample = load_edifact_sample('edifact_sample_mscons_1.txt') 15 | mscons = from_string(mscons_sample) 16 | self.assertEqual(mscons.data['transaction_parties'][0].elements['party_identifier'][0].data[0], 'SU') 17 | self.assertEqual(mscons.data['transaction_parties'][1].elements['party_identifier'][0].data[0], 'BY') 18 | -------------------------------------------------------------------------------- /tests/unit_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FriedrichK/python-edifact/77a7cf3d053fb4dbb21e61559d603ab609f96e19/tests/unit_tests/__init__.py -------------------------------------------------------------------------------- /tests/unit_tests/messages/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FriedrichK/python-edifact/77a7cf3d053fb4dbb21e61559d603ab609f96e19/tests/unit_tests/messages/__init__.py -------------------------------------------------------------------------------- /tests/unit_tests/messages/test_base.py: -------------------------------------------------------------------------------- 1 | """Tests the basic Message functionality.""" 2 | 3 | import unittest 4 | import mock 5 | 6 | from edifact.messages.base import Message 7 | 8 | 9 | class MessagesBaseTest(unittest.TestCase): 10 | 11 | def test_fails_basic_validation_for_unh(self): 12 | mock_source_without_unh = "NOPE+1+MSCONS:D:01B:UN:EAN004'BGM+94E::9+6078+9'DTM+137:20020204:102'NAD+SU+5071615111110::9'NAD+BY+5098765111111::9'" 13 | mock_una = mock.MagicMock() 14 | self.assertRaises(ValueError, Message, mock_una, src_string=mock_source_without_unh) 15 | 16 | def test_fails_basic_validation_for_bgm(self): 17 | mock_source_without_unh = "UNH+1+MSCONS:D:01B:UN:EAN004'NOPE+94E::9+6078+9'DTM+137:20020204:102'NAD+SU+5071615111110::9'NAD+BY+5098765111111::9'" 18 | mock_una = mock.MagicMock() 19 | self.assertRaises(ValueError, Message, mock_una, src_string=mock_source_without_unh) 20 | 21 | def test_fails_basic_validation_for_unt(self): 22 | mock_source_without_unh = "UNH+1+MSCONS:D:01B:UN:EAN004'BGM+94E::9+6078+9'DTM+137:20020204:102'NAD+SU+5071615111110::9'NAD+BY+5098765111111::9'" 23 | mock_una = mock.MagicMock() 24 | self.assertRaises(ValueError, Message, mock_una, src_string=mock_source_without_unh) -------------------------------------------------------------------------------- /tests/unit_tests/test_helpers.py: -------------------------------------------------------------------------------- 1 | """Tests the core functionality.""" 2 | 3 | import unittest 4 | 5 | from edifact.helpers import separate_segments, separate_components 6 | 7 | 8 | class HelperTest(unittest.TestCase): 9 | """Test helper functions.""" 10 | 11 | def test_separates_segments_as_expected_with_release_character_and_newline(self): 12 | src_string="UNH+1+MSCONS:D:01B:UN:EAN004'\nBGM+94E::9+6078+9' NAD+SU+++Papa John?'s'" 13 | expected = ["UNH+1+MSCONS:D:01B:UN:EAN004'", "BGM+94E::9+6078+9'", "NAD+SU+++Papa John?'s'"] 14 | actual = separate_segments(src_string) 15 | self.assertEqual(actual, expected) 16 | 17 | def test_separates_components_as_expected_with_release_character(self): 18 | src_string="NAD+SU+++Papa John?'s:business:food:pizza'" 19 | expected = ["NAD", "SU", "", "", ["Papa John?'s", "business", "food", "pizza"]] 20 | actual = separate_components(src_string) 21 | self.assertEqual(actual, expected) 22 | --------------------------------------------------------------------------------