├── .github └── workflows │ ├── publish-build.yml │ └── validate-build.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── mtconnect ├── __init__.py ├── agent.py ├── device.py ├── error.py ├── helper.py ├── loghandler.py ├── standard_list.py ├── storage.py └── xmlhelper.py ├── poetry.lock ├── pyproject.toml ├── scripts ├── format_package.sh ├── lint_package.sh ├── publish_package.sh ├── run_container.sh ├── run_develop_container.sh ├── set_version.sh └── test_package.sh └── tests ├── __init__.py ├── test_agent.py ├── test_device.xml ├── test_storage.py └── test_xml.xml /.github/workflows/publish-build.yml: -------------------------------------------------------------------------------- 1 | name: Publish Build 2 | 3 | on: 4 | release: 5 | types: [published] 6 | push: 7 | # support traditional versions, and dev versions 8 | tags: ["*.*.*","*.*.*-*"] 9 | 10 | jobs: 11 | 12 | publish: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Build the Docker image 19 | run: | 20 | export DOCKER_IMAGE=mtconnect-python:$(date +%s) 21 | echo "DOCKER_IMAGE=${DOCKER_IMAGE}" >> $GITHUB_ENV 22 | docker build . --file Dockerfile --tag ${DOCKER_IMAGE} 23 | - name: Bump Version 24 | run: | 25 | ./scripts/run_container.sh ${DOCKER_IMAGE} scripts/set_version.sh "${GITHUB_REF_NAME}" 26 | echo "VERSION_CHANGE=$(git diff --exit-code)" >> $GITHUB_ENV 27 | - name: Commit Change 28 | if: ${{ vars.VERSION_CHANGE == '1' }} 29 | run: | 30 | git add . 31 | git commit -m 'Bump Version' 32 | git tag -d ${GITHUB_REF_NAME} 33 | git tag ${GITHUB_REF_NAME} 34 | git push --force origin ${GITHUB_REF_NAME} -------------------------------------------------------------------------------- /.github/workflows/validate-build.yml: -------------------------------------------------------------------------------- 1 | name: Validate Build 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | 11 | validate: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Build the Docker image 18 | run: | 19 | export DOCKER_IMAGE=mtconnect-python:$(date +%s) 20 | echo "DOCKER_IMAGE=${DOCKER_IMAGE}" >> $GITHUB_ENV 21 | docker build . --file Dockerfile --tag ${DOCKER_IMAGE} 22 | - name: Test Format 23 | run: ./scripts/run_container.sh ${DOCKER_IMAGE} scripts/format_package.sh --check 24 | - name: Test Package 25 | run: ./scripts/run_container.sh ${DOCKER_IMAGE} scripts/test_package.sh -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-slim as base 2 | 3 | WORKDIR /data 4 | 5 | # Install deps 6 | RUN pip3 install poetry && \ 7 | apt update && \ 8 | apt install git -y 9 | 10 | # Copy depdencies and install base packages 11 | COPY pyproject.toml poetry.lock ./ 12 | RUN poetry config virtualenvs.create false && \ 13 | poetry install --no-root 14 | 15 | # Copy rest of the repo 16 | COPY . . 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | IMAGE_NAME?=mtconnect-python 3 | build: 4 | docker build --tag ${IMAGE_NAME} . 5 | 6 | develop: build 7 | ./scripts/run_develop_container.sh ${IMAGE_NAME} bash 8 | 9 | run: build 10 | ./scripts/run_container.sh ${IMAGE_NAME} bash 11 | 12 | test: build 13 | ./scripts/run_develop_container.sh ${IMAGE_NAME} scripts/test_package.sh 14 | 15 | test.format: build 16 | ./scripts/run_develop_container.sh ${IMAGE_NAME} scripts/format_package.sh --check 17 | 18 | 19 | format: build 20 | ./scripts/run_develop_container.sh ${IMAGE_NAME} scripts/format_package.sh 21 | 22 | 23 | lint: build 24 | ./scripts/run_develop_container.sh ${IMAGE_NAME} scripts/lint_package.sh 25 | 26 | PYPI_USER?=${PYPI_USER} 27 | PYPI_PASSWORD?=${PYPI_PASSWORD} 28 | publish: build 29 | ./scripts/run_develop_container.sh ${IMAGE_NAME} scripts/publish_package.sh 30 | 31 | # This is only here for completeness. This should only be called via CI 32 | VERSION?=${VERSION} 33 | version: build 34 | ./scripts/run_develop_container.sh ${IMAGE_NAME} scripts/set_version.sh ${VERSION} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MTConnect-Python-Agent 2 | ## Purpose 3 | Python agent for MTConnect applications. 4 | 5 | This library helps store, and format data for MTConnect. The MTConnect standard can be found at [mtconnect.org](https://www.mtconnect.org). This package does not handle the HTTP aspect of the standard just the data processing. It is designed to be used in conjunction with a wsgi interface like flask or django. 6 | 7 | At the time of writting the library is not feature complete. The current implementation is to get a minimal MTConnect instance operational. This library supports Devices, Components, DataItems, and Steams. As well as the probe, current, and sample commands. 8 | 9 | ## Use 10 | To use this library first import the `mtconnect` module. 11 | 12 | ``` 13 | import mtconnect 14 | ``` 15 | 16 | Then create an instance of the MTConnect Agent 17 | 18 | ``` 19 | instance = mtconnect.MTConnect() 20 | ``` 21 | 22 | 23 | The instance requires a xml file with the device descriptions. The default location is `./device.xml`. This can be changed by setting the loc value as shown below. This file is in the format of the `Device` element xml and will be returned by the `probe` command. Be aware that only one device is currently supported. There are plans to support multiple devices in the future. 24 | 25 | ``` 26 | instance = mtconnect.MTconnect(loc='./other_device.xml') 27 | ``` 28 | 29 | 30 | The instance creation also sets the hostname. This hostname is required by the mtconnect standard in the header of all responses. This can be set by providing the hostname variable as shown below. 31 | 32 | ``` 33 | instance = mtconnect.MTconnect(hostname='10.0.0.1:8000') 34 | ``` 35 | 36 | 37 | To push data to the buffer use the following command. The `dataId` is the same as the `id` in the `./device.xml` file. Value must be of the right type or it will error. 38 | ``` 39 | instance.push_data(dataId, value) 40 | ``` 41 | 42 | To access the commands you can run either of the following commands. The corispond to the `probe`, `sample`, `current` endpoints in the mtconnect standard. To filter by uuid/name use xpath in the format `//*[@name=]` or `//*[@id=]` though this functionality is still being worked on and is not fully functional. 43 | 44 | ``` 45 | instance.probe() 46 | instance.sample(path=None, start=None, count=None) 47 | instance.current(at=None, path=None) 48 | ``` 49 | ## Example 50 | Here is a working example using flask. If you would like to see a working implementaiton in a real world example take a look at [this repository](https://github.com/RPIForge/octoprint-companion) 51 | 52 | To run this example first install flask and mtconnect using `pip3 install --upgrade mtconnect flask`. And then start the agent with `flask_app= python3 -m flask run`. For example if the code is saved in `mttest.py` the command would be `FLASK_APP=mttest python3 -m flask run` 53 | 54 | ``` 55 | from flask import Flask, Response, request 56 | from mtconnect import MTConnect 57 | import json 58 | 59 | agent = MTConnect() 60 | app = Flask(__name__) 61 | 62 | 63 | @app.route('/probe') 64 | def probe(): 65 | response = agent.probe() 66 | return Response(response.get_xml(),status=response.get_status(), mimetype='text/xml') 67 | 68 | @app.route('/current', defaults={'identifier': None}) 69 | @app.route('//current') 70 | def current(identifier): 71 | path = request.args.get('path', None) 72 | at = request.args.get('at', None, type=int) 73 | 74 | if(identifier is not None): 75 | path = ".//*[@id='{}'] | .//*[@name='{}']".format(identifier,identifier) 76 | 77 | response = agent.current(at, path) 78 | return Response(response.get_xml(),status=response.get_status(), mimetype='text/xml') 79 | 80 | @app.route('/sample', defaults={'identifier': None}) 81 | @app.route('//sample') 82 | def sample(identifier): 83 | path = request.args.get('path', None) 84 | start = request.args.get('from', None, type=int) 85 | count = request.args.get('count',None, type=int) 86 | 87 | if(identifier is not None): 88 | path = ".//*[@id='{}'] | .//*[@name='{}']".format(identifier,identifier) 89 | 90 | response = agent.sample(path,start,count) 91 | return responseResponse(response.get_xml(),status=response.get_status(), mimetype='text/xml') 92 | 93 | ## Push data to the agent 94 | agent.push_data('avail','AVAILABLE') 95 | ``` 96 | 97 | ## Development 98 | 99 | There are still limitations to this library. Here is a list of features that are still needed that are being worked on. They are in no particular order 100 | 101 | * Disk buffer. Allow the user to store data on disk using h5py instead of in memory 102 | * Implement multiple devices. Current each instance only supports one device. This should be a relatively easy fix in the push_data function 103 | * Implement entire MTConnect standard. Currently I do not handle assets or interfaces 104 | -------------------------------------------------------------------------------- /mtconnect/__init__.py: -------------------------------------------------------------------------------- 1 | from .agent import MTConnect 2 | from .loghandler import MTLogger 3 | -------------------------------------------------------------------------------- /mtconnect/agent.py: -------------------------------------------------------------------------------- 1 | # general imports 2 | import os 3 | import uuid 4 | from xml.etree import ElementTree 5 | from datetime import datetime 6 | from numbers import Number 7 | 8 | # mtconnect imports 9 | from .storage import MTBuffer, MTDataEntity 10 | from .xmlhelper import read_devices, process_path 11 | from .error import MTInvalidRequest, MTInvalidRange 12 | from .device import MTComponent, MTDevice 13 | from .loghandler import MTLogger 14 | 15 | 16 | # 17 | # Helper Function for 18 | class MTResponse: 19 | # ! Use: Handle data for MTConnect response 20 | # ? Data: xml data and response code 21 | 22 | # xml and status code 23 | xml = None 24 | status_code = 200 25 | 26 | def __init__(self, xml, status_code): 27 | self.xml = xml 28 | self.status_code = status_code 29 | 30 | def get_xml(self): 31 | return self.xml 32 | 33 | def get_status(self): 34 | return self.status_code 35 | 36 | 37 | class MTConnect: 38 | # ! Use: Handle MTConnect agent 39 | # ? Data: 40 | # instanceID 41 | instanceId = None 42 | hostname = None 43 | 44 | # item buffer 45 | buffer = None 46 | asset_buffer = None 47 | 48 | # dictionary of devices 49 | device_dict = None 50 | device_xml = None 51 | 52 | # used for traversal 53 | item_dict = {} # list of a 54 | component_dict = {} # list of components 55 | 56 | def __init__(self, loc="./device.xml", hostname="http://0.0.0.0:80"): 57 | MTLogger.info("Initializing MTConnect Agent") 58 | # set variables 59 | self.hostname = hostname 60 | 61 | # initalize buffer 62 | self.buffer = MTBuffer() 63 | 64 | # read device information 65 | file_location = os.getenv("MTCDeviceFile", loc) 66 | MTLogger.debug("Reading Devices from {}".format(file_location)) 67 | device_data = read_devices(file_location) 68 | 69 | # Update item dict to contain all items 70 | self.device_dict, self.device_xml = device_data 71 | for device in self.device_dict.values(): 72 | self.item_dict.update(device.item_dict) 73 | self.component_dict.update(device.component_dict) 74 | 75 | # generate instanceId -64bit int uuid4 is 128 so shift it 76 | self.instanceId = str(uuid.uuid4().int & (1 << 64) - 1) 77 | MTLogger.debug("Settings UUID to {}".format(self.instanceId)) 78 | 79 | # create inital values for item 80 | for item in self.item_dict.values(): 81 | self.push_data(item.id, "UNAVAILABLE") 82 | 83 | MTLogger.info("Finished Initalizing MTConnect Agent") 84 | 85 | # 86 | # Accessor Functions 87 | # 88 | def get_device_list(self): 89 | return list(self.device_dict.values()) 90 | 91 | def get_device(self, name=None): 92 | if name is None: 93 | return self.get_device_list()[0] 94 | else: 95 | return self.device_dict[name] 96 | 97 | # 98 | # Modifier Functions 99 | # 100 | def set_device_id(self, device, id): 101 | device.set_id(id) 102 | 103 | def set_device_name(self, device, name): 104 | device.set_name(name) 105 | 106 | # 107 | # Data Functions 108 | # 109 | 110 | # validate pushing data 111 | def get_dataId(self, dataId): 112 | for device in self.device_dict.values(): 113 | if dataId in device.get_sub_item(): 114 | return device.item_dict[dataId] 115 | raise ValueError("DataID {} is not found".format(dataId)) 116 | 117 | # push data from machines 118 | def push_data(self, dataId, value): 119 | dataItem = self.get_dataId(dataId) 120 | new_data = MTDataEntity(dataItem, value) 121 | self.buffer.push(new_data) 122 | 123 | # 124 | # MTConnect/XML Functions 125 | # 126 | 127 | def get_header(self): 128 | header_element = ElementTree.Element("Header") 129 | header_element.set("version", "1.6.0.0") 130 | header_element.set("instanceId", str(self.instanceId)) 131 | header_element.set("sender", str(self.hostname)) 132 | header_element.set( 133 | "creationTime", datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") 134 | ) 135 | header_element.set("bufferSize", str(self.buffer.size())) 136 | header_element.set("assetBufferSize", "0") 137 | header_element.set("assetCount", "0") 138 | return header_element 139 | 140 | # run MTConnect probe command 141 | def probe(self): 142 | root_container = ElementTree.Element("MTConnectDevices") 143 | root_container.append(self.get_header()) 144 | 145 | device_container = ElementTree.SubElement(root_container, "Devices") 146 | for device in self.device_dict: 147 | device_container.append(self.device_dict[device].xml_data) 148 | 149 | return MTResponse(ElementTree.tostring(root_container).decode(), 200) 150 | 151 | # run MTConnect sample command 152 | def sample(self, path=None, start=None, count=None): 153 | # data validation 154 | 155 | # if count is given but not start 156 | if start is None and count is not None: 157 | if count < 0: 158 | start = self.buffer.last_sequence 159 | else: 160 | start = self.buffer.first_sequence 161 | 162 | if start is None: 163 | start = self.buffer.first_sequence 164 | 165 | if count is None: 166 | count = 100 167 | 168 | error = None 169 | if not isinstance(start, Number) or start < 0: 170 | error = MTInvalidRequest(self, "Start must be a non negative number") 171 | 172 | if not isinstance(count, Number): 173 | error = MTInvalidRequest(self, "Count must be a number").to_xml() 174 | 175 | if self.buffer.empty(): 176 | error = MTInvalidRange(self, "Buffer is currently empty") 177 | 178 | if start < self.buffer.first_sequence or start > self.buffer.last_sequence: 179 | error = MTInvalidRange( 180 | self, 181 | "Start must be between {} and {}".format( 182 | self.buffer.first_sequence, self.buffer.last_sequence 183 | ), 184 | ) 185 | 186 | if abs(count) > self.buffer.size(): 187 | error = MTInvalidRange( 188 | self, "Count must not be greater than {}".format(self.buffer.size()) 189 | ) 190 | 191 | if error: 192 | return MTResponse(error.to_xml(), 400) 193 | 194 | # put count and start into usable formats 195 | if count < 0: 196 | start = start + count 197 | count = abs(count) 198 | 199 | end = start + count 200 | 201 | # apply path variable 202 | item_set = self.get_item_list(path) 203 | 204 | # get all itesm last dataitem 205 | sample_dict = {} 206 | for item in item_set: 207 | 208 | # get data 209 | data = item.get_data(start, end) 210 | 211 | # if device had no data yet then initialize 212 | if item.device not in sample_dict: 213 | sample_dict[item.device] = {} 214 | 215 | # if component has no data then initialize 216 | if item.parent_component not in sample_dict[item.device]: 217 | sample_dict[item.device][item.parent_component] = {} 218 | 219 | if item.category not in sample_dict[item.device][item.parent_component]: 220 | sample_dict[item.device][item.parent_component][item.category] = [] 221 | 222 | sample_dict[item.device][item.parent_component][item.category].append(data) 223 | 224 | # format the output xml 225 | sample_stream = self.format_stream_xml(sample_dict) 226 | 227 | # format final xml 228 | root_container = ElementTree.Element("MTConnectStreams") 229 | root_container.append(self.get_header()) 230 | root_container.append(sample_stream) 231 | return MTResponse(ElementTree.tostring(root_container).decode(), 200) 232 | 233 | # run MTConnnect current command 234 | def current(self, at=None, path=None): 235 | # data validation 236 | if at is None: 237 | at = self.buffer.last_sequence 238 | 239 | error = None 240 | if not isinstance(at, Number) or at < 0: 241 | error = MTInvalidRequest(self, "At must be a non negative number") 242 | 243 | if self.buffer.empty(): 244 | error = MTInvalidRange(self, "Buffer is currently empty") 245 | 246 | if at < self.buffer.first_sequence or at > self.buffer.last_sequence: 247 | error = MTInvalidRange( 248 | self, 249 | "At must be between {} and {}".format( 250 | self.buffer.first_sequence, self.buffer.last_sequence 251 | ), 252 | ) 253 | 254 | if error: 255 | return MTResponse(error.to_xml(), 400) 256 | 257 | # get all sub items from path 258 | item_set = self.get_item_list(path) 259 | 260 | # get all itesm last dataitem 261 | current_dict = {} 262 | for item in item_set: 263 | 264 | # get data 265 | data = item.get_current(at) 266 | 267 | # if device had no data yet then initialize 268 | if item.device not in current_dict: 269 | current_dict[item.device] = {} 270 | 271 | # if component has no data then initialize 272 | if item.parent_component not in current_dict[item.device]: 273 | current_dict[item.device][item.parent_component] = {} 274 | 275 | if item.category not in current_dict[item.device][item.parent_component]: 276 | current_dict[item.device][item.parent_component][item.category] = [] 277 | 278 | current_dict[item.device][item.parent_component][item.category].append(data) 279 | 280 | # format the output xml 281 | current_stream = self.format_stream_xml(current_dict) 282 | 283 | # format final xml 284 | root_container = ElementTree.Element("MTConnectStreams") 285 | root_container.append(self.get_header()) 286 | root_container.append(current_stream) 287 | return MTResponse(ElementTree.tostring(root_container).decode(), 200) 288 | 289 | # get list of items to search 290 | def get_item_list(self, path=None): 291 | # apply path variable 292 | if path is not None: 293 | component_list = process_path( 294 | self.device_xml, path, self.item_dict, self.component_dict 295 | ) 296 | else: 297 | component_list = list(self.device_dict.values()) 298 | 299 | # get all sub items from path 300 | item_set = set() 301 | for component in component_list: 302 | item_set = item_set.union(set(component.get_all_sub_items())) 303 | return item_set 304 | 305 | # format data into xml 306 | def format_stream_xml(self, data_dictionary): 307 | stream = ElementTree.Element("Streams") 308 | 309 | # format the output xml 310 | for device in data_dictionary: 311 | # create root device stream 312 | device_element = ElementTree.SubElement(stream, "DeviceStream") 313 | device_element.set("name", device.name) 314 | device_element.set("uuid", device.uuid) 315 | 316 | # loop through all data iterms and compoments 317 | for component in data_dictionary[device]: 318 | 319 | # get root component stream 320 | stream_element = ElementTree.SubElement( 321 | device_element, "ComponentStream" 322 | ) 323 | if isinstance(component, MTComponent): 324 | stream_element.set("component", component.type) 325 | elif isinstance(component, MTDevice): 326 | stream_element.set("component", "Device") 327 | stream_element.set("name", component.name) 328 | stream_element.set("componentId", component.id) 329 | 330 | # get data 331 | component_data = data_dictionary[device][component] 332 | 333 | # For each category add data to xml 334 | for category in component_data: 335 | sample_container = ElementTree.SubElement( 336 | stream_element, category.title() + "s" 337 | ) 338 | for item_list in component_data[category]: 339 | if isinstance(item_list, MTDataEntity): 340 | sample_container.append(item_list.get_xml()) 341 | else: 342 | for item in item_list: 343 | sample_container.append(item.get_xml()) 344 | return stream 345 | 346 | def error(self, error_text): 347 | pass 348 | -------------------------------------------------------------------------------- /mtconnect/device.py: -------------------------------------------------------------------------------- 1 | # general imports 2 | import uuid 3 | from xml.etree import ElementTree 4 | 5 | # import mtcitems 6 | from .standard_list import MTC_DataID_list 7 | from .storage import MTDataEntity 8 | 9 | # class helper for MTGeneric to display component tree 10 | def tree_helper(component, level=0): 11 | output_string = "+" + ("-" * 5) * level + str(component.id) + "\n" 12 | for item in component.get_sub_items(): 13 | output_string = ( 14 | output_string + "+" + ("-" * 5) * (level + 1) + "+" + str(item.id) + "\n" 15 | ) 16 | 17 | component_list = component.get_sub_components() 18 | 19 | for comp in component_list: 20 | output_string = output_string + tree_helper(comp, level + 1) 21 | 22 | return output_string 23 | 24 | 25 | # class helper for MTGeneric to get all subcomponents 26 | def item_helper(component): 27 | output_list = [] 28 | item_list = component.get_sub_items() 29 | for item in item_list: 30 | output_list.append(item) 31 | 32 | component_list = component.get_sub_components() 33 | for component in component_list: 34 | output_list = output_list + item_helper(component) 35 | 36 | return output_list 37 | 38 | 39 | class MTGeneric: 40 | # ! Use: Generic item for all parts of device. Includes components and data_list 41 | # ? Data: Holds id,name, and attributes as well as parent componnetn and data_list 42 | 43 | # generic variables 44 | id = None 45 | name = None 46 | 47 | # attributes 48 | attributes = {} 49 | 50 | # parrent component 51 | parent_component = None 52 | 53 | def __init__(self, id, name, component): 54 | if id is None: 55 | raise ValueError("Missing required id for MTC") 56 | 57 | self.id = id 58 | self.name = name 59 | self.parent_component = component 60 | 61 | self.attributes = {} 62 | self.data_list = [] 63 | 64 | # 65 | # Modifiers 66 | # 67 | def set_name(self, name): 68 | self.name = name 69 | 70 | def set_id(self, id): 71 | self.id = id 72 | 73 | # add attribute to container 74 | def add_attribute(self, name, value): 75 | self.attributes[name] = value 76 | 77 | def __str__(self): 78 | return "{} with id {} and name {} with parent {}".format( 79 | type(self), self.id, self.name, self.parent_component 80 | ) 81 | 82 | 83 | class MTGenericContainer(MTGeneric): 84 | # generic variables 85 | description = None 86 | 87 | # variables used for storage of sub items 88 | sub_components = {} 89 | items = {} 90 | 91 | # raw xml data 92 | xml_data = None 93 | 94 | def __init__(self, id, name, xml_data, parent_component, description=None): 95 | super().__init__(id, name, parent_component) 96 | 97 | self.xml_data = xml_data 98 | self.description = description 99 | self.sub_components = {} 100 | self.items = {} 101 | 102 | def set_name(self, name): 103 | super().set_name(name) 104 | self.xml_data.set("name", name) 105 | 106 | def set_id(self, id): 107 | super().set_id(id) 108 | self.xml_data.set("id", id) 109 | 110 | # add subaccount directly to container 111 | def add_subcomponent(self, Component): 112 | self.sub_components[Component.id] = Component 113 | 114 | # add item directly to conrainter 115 | def add_item(self, Item): 116 | self.items[Item.id] = Item 117 | 118 | # get list of subcomponents 119 | def get_sub_components(self): 120 | return list(self.sub_components.values()) 121 | 122 | # get list of dataitems 123 | def get_sub_items(self): 124 | return list(self.items.values()) 125 | 126 | # get all sub items of component 127 | def get_all_sub_items(self): 128 | return item_helper(self) 129 | 130 | def display_tree(self): 131 | return tree_helper(self) 132 | 133 | def generate_xml(self): 134 | return ElementTree.tostring(self.xml_data) 135 | 136 | 137 | class MTDevice(MTGenericContainer): 138 | # generic variables 139 | uuid = None 140 | 141 | # variables used for traversal of sub items 142 | item_dict = {} 143 | component_dict = {} 144 | 145 | def __init__(self, id, name, xml_data, unique_id=None, description=None): 146 | super().__init__(id, name, xml_data, None, description) 147 | 148 | if unique_id is None: 149 | unique_id = str(uuid.uuid4()) 150 | 151 | self.uuid = unique_id 152 | self.item_dict = {} 153 | self.component_dict = {} 154 | 155 | # 156 | # Modifier 157 | # 158 | def add_subcomponent(self, Component): 159 | super().add_subcomponent(Component) 160 | self.component_dict[Component.id] = Component 161 | 162 | def add_item(self, Item): 163 | super().add_item(Item) 164 | self.item_dict[Item.id] = Item 165 | 166 | # add device to device list for traversal 167 | def add_sub_item(self, Item): 168 | self.item_dict[Item.id] = Item 169 | 170 | # add component to device list for traversal 171 | def add_sub_component(self, Component): 172 | self.component_dict[Component.id] = Component 173 | 174 | def get_sub_item(self): 175 | return self.item_dict.keys() 176 | 177 | 178 | class MTComponent(MTGenericContainer): 179 | # descriptor variables 180 | type = None 181 | 182 | # parent variables 183 | device = None 184 | 185 | def __init__( 186 | self, id, name, type, xml_data, parent_component, device, description=None 187 | ): 188 | super().__init__(id, name, xml_data, parent_component, description) 189 | 190 | self.type = type 191 | self.device = device 192 | 193 | 194 | class MTDataItem(MTGeneric): 195 | category = None 196 | type = None 197 | 198 | # parent variable 199 | device = None 200 | 201 | # Last value thats outside the buffer 202 | ##used in current 203 | last_value = None 204 | 205 | # list of data entity assigned 206 | data_list = [] 207 | 208 | def __init__(self, id, name, type, category, device, component): 209 | 210 | category = category.upper() 211 | if None in [id, type, category, component, device]: 212 | raise ValueError("Missing required value for DataItem") 213 | 214 | if category not in ["SAMPLE", "CONDITION", "EVENT"]: 215 | raise ValueError("{} is not a valid category".format(category)) 216 | 217 | if type not in MTC_DataID_list: 218 | raise ValueError("{} is not a valid type".format(type)) 219 | 220 | if id in device.get_sub_item(): 221 | raise ValueError("id must be unique. {} has been used".format(id)) 222 | 223 | super().__init__(id, name, component) 224 | 225 | self.type = type 226 | self.category = category 227 | 228 | self.device = device 229 | 230 | # Handle DataEntitys assigned 231 | def push_data(self, DataEntity): 232 | if self.last_value is None: 233 | self.last_value = DataEntity 234 | 235 | self.data_list.append(DataEntity) 236 | 237 | def pop_data(self): 238 | self.data_list.pop(0) 239 | 240 | def get_data(self, start_sequence=float("-inf"), end_sequence=float("inf")): 241 | output_list = [] 242 | for data in self.data_list: 243 | if ( 244 | data.sequence_number > start_sequence 245 | and data.sequence_number <= end_sequence 246 | ): 247 | output_list.append(data) 248 | return output_list 249 | 250 | def get_current(self, seq=float("inf")): 251 | data = self.get_data(end_sequence=seq) 252 | if not data: 253 | return self.last_value 254 | 255 | return data[-1] 256 | -------------------------------------------------------------------------------- /mtconnect/error.py: -------------------------------------------------------------------------------- 1 | from xml.etree import ElementTree 2 | 3 | 4 | class MTError: 5 | name = "" 6 | message = "" 7 | 8 | agent = None 9 | 10 | def __init__(self, agent, message): 11 | self.agent = agent 12 | self.message = message 13 | 14 | def to_xml(self): 15 | root_container = ElementTree.Element("MTConnectError") 16 | root_container.append(self.agent.get_header()) 17 | 18 | error_container = ElementTree.SubElement(root_container, "Errors") 19 | error = ElementTree.SubElement(error_container, "Error") 20 | error.set("errorCode", self.name) 21 | error.text = self.message 22 | 23 | return ElementTree.tostring(root_container).decode() 24 | 25 | 26 | class MTInvalidRequest(MTError): 27 | name = "MTInvalidRequest" 28 | 29 | 30 | class MTInvalidRange(MTError): 31 | name = "MTInvalidRange" 32 | -------------------------------------------------------------------------------- /mtconnect/helper.py: -------------------------------------------------------------------------------- 1 | # helper functions 2 | # convert type to pascal 3 | def type_to_pascal(type): 4 | words = type.split("_") 5 | output_string = "" 6 | for word in words: 7 | if word in ["PH", "AC", "DC"]: 8 | output_string = output_string + word 9 | else: 10 | output_string = output_string + word.title() 11 | return output_string 12 | -------------------------------------------------------------------------------- /mtconnect/loghandler.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | MTLogger = logging.getLogger(__name__) 5 | level_string = os.getenv("MTCLogLevel", "").upper() 6 | level = logging.INFO 7 | if level_string == "ERROR": 8 | level = logging.ERROR 9 | 10 | elif level_string == "WARN": 11 | level = logging.WARN 12 | 13 | elif level_string == "INFO": 14 | level = logging.INFO 15 | 16 | elif level_string == "DEBUG": 17 | level = logging.DEBUG 18 | 19 | MTLogger.setLevel(level) 20 | -------------------------------------------------------------------------------- /mtconnect/standard_list.py: -------------------------------------------------------------------------------- 1 | MTC_DataID_list = [ 2 | "ACTUATOR", 3 | "CHUCK_INTERLOCK", 4 | "COMMUNICATIONS", 5 | "DATA_RANGE", 6 | "DIRECTION", 7 | "END_OF_BAR", 8 | "HARDWARE", 9 | "INTERFACE_STATE", 10 | "LOGIC_PROGRAM", 11 | "MOTION_PROGRAM", 12 | "SYSTEM", 13 | "ACCELERATION", 14 | "ACCUMULATED_TIME", 15 | "ANGULAR_ACCELERATION", 16 | "ANGULAR_VELOCITY", 17 | "AMPERAGE", 18 | "ALTERNATING", 19 | "DIRECT", 20 | "ACTUAL", 21 | "TARGET", 22 | "ANGLE", 23 | "ACTUAL", 24 | "COMMANDED", 25 | "AXIS_FEEDRATE", 26 | "ACTUAL", 27 | "COMMANDED", 28 | "JOG", 29 | "PROGRAMMED", 30 | "RAPID", 31 | "OVERRIDE", 32 | "CLOCK_TIME", 33 | "CONCENTRATION", 34 | "CONDUCTIVITY", 35 | "DISPLACEMENT", 36 | "ELECTRICAL_ENERGY", 37 | "EQUIPMENT_TIMER", 38 | "LOADED", 39 | "WORKING", 40 | "OPERATING", 41 | "POWERED", 42 | "DELAY", 43 | "FILL_LEVEL", 44 | "FLOW", 45 | "FREQUENCY", 46 | "LENGTH", 47 | "STANDARD", 48 | "REMAINING", 49 | "USEABLE", 50 | "LINEAR_FORCE", 51 | "LOAD", 52 | "MASS", 53 | "PATH_FEEDRATE", 54 | "ACTUAL", 55 | "COMMANDED", 56 | "JOG", 57 | "PROGRAMMED", 58 | "RAPID", 59 | "OVERRIDE", 60 | "PATH_POSITION", 61 | "ACTUAL", 62 | "COMMANDED", 63 | "TARGET", 64 | "PROBE", 65 | "PH", 66 | "POSITION", 67 | "ACTUAL", 68 | "COMMANDED", 69 | "PROGRAMMED", 70 | "TARGET", 71 | "POWER_FACTOR", 72 | "PRESSURE", 73 | "PROCESS_TIMER", 74 | "PROCESS", 75 | "DELAY", 76 | "RESISTANCE", 77 | "ROTARY_VELOCITY", 78 | "ACTUAL", 79 | "COMMANDED", 80 | "PROGRAMMED", 81 | "OVERRIDE", 82 | "SOUND_LEVEL", 83 | "NO_SCALE", 84 | "A_SCALE", 85 | "B_SCALE", 86 | "C_SCALE", 87 | "D_SCALE", 88 | "SPINDLE_SPEED", 89 | "ACTUAL", 90 | "COMMANDED", 91 | "OVERRIDE", 92 | "STRAIN", 93 | "TEMPERATURE", 94 | "TENSION", 95 | "TILT", 96 | "TORQUE", 97 | "VOLT_AMPERE", 98 | "VOLT_AMPERE_REACTIVE", 99 | "VELOCITY", 100 | "VISCOSITY", 101 | "VOLTAGE", 102 | "ALTERNATING", 103 | "DIRECT", 104 | "ACTUAL", 105 | "TARGET", 106 | "WATTAGE", 107 | "ACTUAL", 108 | "TARGET", 109 | "ACTUATOR_STATE", 110 | "ALARM", 111 | "ACTIVE_AXES", 112 | "", 113 | "", 114 | "AVAILABILITY", 115 | "AXIS_COUPLING", 116 | "", 117 | "", 118 | "AXIS_FEEDRATE_OVERRIDE", 119 | "JOG", 120 | "PROGRAMMED", 121 | "RAPID", 122 | "AXIS_INTERLOCK", 123 | "AXIS_STATE", 124 | "BLOCK", 125 | "BLOCK_COUNT", 126 | "CHUCK_INTERLOCK", 127 | "MANUAL_UNCLAMP", 128 | "CHUCK_STATE", 129 | "CODE", 130 | "COMPOSITION_STATE", 131 | "ACTION", 132 | "LATERAL", 133 | "MOTION", 134 | "SWITCHED", 135 | "VERTICAL", 136 | "CONTROLLER_MODE", 137 | "CONTROLLER_MODE_OVERRIDE", 138 | "DRY_RUN", 139 | "SINGLE_BLOCK", 140 | "MACHINE_AXIS_LOCK", 141 | "OPTIONAL_STOP", 142 | "TOOL_CHANGE_STOP", 143 | "COUPLED_AXES", 144 | "DIRECTION", 145 | "ROTARY", 146 | "LINEAR", 147 | "DOOR_STATE", 148 | "END_OF_BAR", 149 | "PRIMARY", 150 | "AUXILIARY", 151 | "EMERGENCY_STOP", 152 | "EQUIPMENT_MODE", 153 | "LOADED", 154 | "WORKING", 155 | "OPERATING", 156 | "POWERED", 157 | "DELAY", 158 | "EXECUTION", 159 | "FUNCTIONAL_MODE", 160 | "HARDNESS", 161 | "ROCKWELL", 162 | "VICKERS", 163 | "SHORE", 164 | "BRINELL", 165 | "LEEB", 166 | "MOHS", 167 | "INTERFACE_STATE", 168 | "LINE", 169 | "MAXIMUM", 170 | "MINIMUM", 171 | "LINE_LABEL", 172 | "LINE_NUMBER", 173 | "ABSOLUTE", 174 | "INCREMENTAL", 175 | "MATERIAL", 176 | "MESSAGE", 177 | "OPERATOR_ID", 178 | "PALLET_ID", 179 | "PART_COUNT", 180 | "ALL", 181 | "GOOD", 182 | "BAD", 183 | "TARGET", 184 | "REMAINING", 185 | "PART_ID", 186 | "PART_NUMBER", 187 | "PATH_FEEDRATE_OVERRIDE", 188 | "JOG", 189 | "PROGRAMMED", 190 | "RAPID", 191 | "PATH_MODE", 192 | "POWER_STATE", 193 | "", 194 | "", 195 | "LINE", 196 | "CONTROL", 197 | "POWER_STATUS", 198 | "PROGRAM", 199 | "PROGRAM_EDIT", 200 | "PROGRAM_EDIT_NAME", 201 | "PROGRAM_COMMENT", 202 | "PROGRAM_HEADER", 203 | "ROTARY_MODE", 204 | "ROTARY_VELOCITY_OVERRIDE", 205 | "SERIAL_NUMBER", 206 | "SPINDLE_INTERLOCK", 207 | "TOOL_ASSET_ID", 208 | "TOOL_NUMBER", 209 | "TOOL_OFFSET", 210 | "RADIAL", 211 | "LENGTH", 212 | "USER", 213 | "OPERATOR", 214 | "MAINTENANCE", 215 | "SET_UP", 216 | "WIRE", 217 | "WORKHOLDING_ID", 218 | "WORK_OFFSET", 219 | ] 220 | -------------------------------------------------------------------------------- /mtconnect/storage.py: -------------------------------------------------------------------------------- 1 | import os # enviormentl variabels 2 | import datetime # get currenttime 3 | import numbers # verrify value is number 4 | from xml.etree import ElementTree # generate xml 5 | 6 | # MTConnect Imports 7 | from .helper import type_to_pascal 8 | 9 | 10 | class MTDataEntity: 11 | # ! Use: Handle the individual data values 12 | # ? Data: Holds sequence number, tiemstamp, and data 13 | 14 | # sequence number in buffer and timestamp 15 | sequence_number = None 16 | timestamp = None 17 | 18 | # The MTDataItem that this data refernced 19 | dataItem = None 20 | 21 | # actual value 22 | value = None 23 | 24 | def __init__(self, dataItem, value): 25 | # set values 26 | self.dataItem = dataItem 27 | if dataItem.category == "SAMPLE" and ( 28 | not isinstance(value, numbers.Number) and value != "UNAVAILABLE" 29 | ): 30 | raise ValueError("SAMPLE value must be number") 31 | 32 | if dataItem.category == "EVENT" and not isinstance(value, str): 33 | raise ValueError("EVENT value must be a string") 34 | 35 | if dataItem.category == "CONDITION" and value not in [ 36 | "UNAVAILABLE", 37 | "NORMAL", 38 | "WARNING", 39 | "FAULT", 40 | ]: 41 | raise ValueError( 42 | "CONDITION value must be one of UNAVAILABLE, NORMAL, WARNING, or FAULT" 43 | ) 44 | 45 | self.value = value 46 | self.timestamp = datetime.datetime.utcnow() 47 | 48 | # 49 | # Accessor Functions 50 | # 51 | 52 | # get timestamp as MTCOnnect formated string 53 | def get_time_str(self): 54 | return self.timestamp.strftime("%Y-%m-%dT%H:%M:%SZ") 55 | 56 | # get xml data for current/sample 57 | def get_xml(self): 58 | element = ElementTree.Element(type_to_pascal(self.dataItem.type)) 59 | element.set("dataItemId", self.dataItem.id) 60 | element.set("timestamp", self.get_time_str()) 61 | if self.dataItem.name is not None: 62 | element.set("name", self.dataItem.name) 63 | 64 | element.set("sequence", str(self.sequence_number)) 65 | 66 | element.text = str(self.value) 67 | return element 68 | 69 | # 70 | # Mutator Functions 71 | # 72 | def set_sequence(self, number): 73 | self.sequence_number = number 74 | return self.sequence_number 75 | 76 | 77 | class MTBuffer: 78 | # ! Use: Handle buffer operations for MTConnect agent 79 | # ? Data: Holds all data about sequencing and previous values 80 | 81 | # buffer variables 82 | buffer = None 83 | buffer_size = None 84 | 85 | # keep track of buffer location 86 | first_sequence = None # oldest piece of data 87 | last_sequence = None # newest piece of data 88 | buffer_pos = 0 # keep track of place in buffer 89 | 90 | # 91 | # Constructor Functions 92 | # 93 | def __init__(self, buffer_length=None): 94 | # get buffer size 95 | if buffer_length is None: 96 | buffer_length = int(os.environ.get("BUFFER_SIZE", 16384)) 97 | 98 | # initialize buffer 99 | self.buffer_size = buffer_length 100 | self.buffer = [None] * self.buffer_size 101 | 102 | # 103 | # Accessor Functions 104 | # 105 | def get_buffer(self): 106 | return self.buffer 107 | 108 | def get_data(self, seq, count=None): 109 | # if requested sequence is to below sequence 110 | if seq < self.first_sequence: 111 | return ([], self.first_sequence) 112 | 113 | # if count is none get max count 114 | if count == None: 115 | count = self.buffer_size 116 | 117 | # if count is <= 0 then return nothing 118 | if count <= 0: 119 | return ([], seq) 120 | 121 | # get location in the buffer 122 | buffer_loc = seq - self.first_sequence 123 | 124 | # get end position and adjust if it goes over 125 | end_pos = buffer_loc + count 126 | if end_pos >= self.buffer_size: 127 | end_pos = self.buffer_size 128 | 129 | # get next_sequence calculations 130 | next_sequence = self.buffer[end_pos - 1].sequence_number + 1 131 | return (self.buffer[buffer_loc:end_pos], next_sequence) 132 | 133 | def empty(self): 134 | return self.first_sequence is None 135 | 136 | def size(self): 137 | return self.buffer_size 138 | 139 | # 140 | # Mutator Functions 141 | # 142 | def push(self, DataElement): 143 | # if DataElement is not the correct type 144 | if not isinstance(DataElement, MTDataEntity): 145 | raise TypeError("DataElement is not of type MTDataEntity") 146 | 147 | # if sequence number is greater than the buffer size 148 | if self.buffer_pos >= self.buffer_size: 149 | # get last item 150 | last_item = self.buffer.pop(0) 151 | 152 | # remove last item from refernced Device 153 | last_item.dataItem.pop_data() 154 | 155 | # update last item list 156 | last_item.last_value = last_item 157 | 158 | # update sequence 159 | self.first_sequence = self.buffer[0].sequence_number 160 | 161 | # add last item 162 | self.buffer.append(None) 163 | self.buffer_pos = self.buffer_size - 1 164 | 165 | # get new sequence number 166 | if self.last_sequence is None: 167 | sequence_number = 1 168 | else: 169 | sequence_number = self.last_sequence + 1 170 | 171 | # set sequence number and add to list 172 | DataElement.set_sequence(sequence_number) 173 | self.buffer[self.buffer_pos] = DataElement 174 | 175 | # add data to refernced dataItem 176 | DataElement.dataItem.push_data(DataElement) 177 | 178 | # update counter and static variables 179 | self.buffer_pos = self.buffer_pos + 1 180 | self.last_sequence = sequence_number 181 | if self.first_sequence is None: 182 | self.first_sequence = sequence_number 183 | -------------------------------------------------------------------------------- /mtconnect/xmlhelper.py: -------------------------------------------------------------------------------- 1 | # general imports 2 | from xml.etree import ElementTree 3 | 4 | # MTConnect imports 5 | from .device import MTDevice, MTComponent, MTDataItem 6 | 7 | 8 | # 9 | # AGENT XML Helpers 10 | # 11 | def process_path(device_xml, path, item_dict, component_dict): 12 | xml_list = device_xml.findall(path) 13 | component_list = [] 14 | for element in xml_list: 15 | 16 | id = element.get("id") 17 | if id in item_dict: 18 | component_list.append(item_dict[id]) 19 | 20 | if id in component_dict: 21 | component_list.append(component_dict[id]) 22 | return component_list 23 | 24 | 25 | # 26 | # DEVICE XML Helpers 27 | # 28 | 29 | # function to process all of the dataitems on a component 30 | def process_dataitem(item_list, device, component): 31 | for item in item_list: 32 | # get required objects 33 | id = item.get("id") 34 | category = item.get("category") 35 | type = item.get("type") 36 | name = item.get("name") 37 | 38 | new_item = MTDataItem(id, name, type, category, device, component) 39 | 40 | for attribute in item.items(): 41 | new_item.add_attribute(attribute[0], attribute[1]) 42 | component.add_item(new_item) 43 | device.add_sub_item(new_item) 44 | 45 | 46 | # function to recursively add components 47 | def process_components(component_list, device, parent_component): 48 | for component in component_list: 49 | # get component attributes 50 | name = component.get("name") 51 | type = component.tag 52 | id = component.get("id") 53 | description = component.find("Description") 54 | 55 | # get the text value for the description 56 | if description is not None: 57 | description = description.text 58 | 59 | # create top level component 60 | new_component = MTComponent( 61 | id, name, type, component, parent_component, device, description 62 | ) 63 | device.add_sub_component(new_component) 64 | 65 | parent_component.add_subcomponent(new_component) 66 | 67 | # get list of attributes 68 | for attribute in component.items(): 69 | new_component.add_attribute(attribute[0], attribute[1]) 70 | 71 | # get list of data items 72 | component_items = component.find("DataItems") 73 | if component_items is not None: 74 | process_dataitem(component_items, device, new_component) 75 | 76 | # get list of subcomponents 77 | sub_component_item = component.find("Components") 78 | if sub_component_item is not None: 79 | sub_component_list = sub_component_item.getchildren() 80 | process_components(sub_component_list, device, new_component) 81 | 82 | 83 | # read device xml from file 84 | def read_devices(file): 85 | # read data file 86 | try: 87 | device_tree = ElementTree.parse(file) 88 | except FileNotFoundError: 89 | raise ValueError("{} is not a valid file".format(file)) 90 | 91 | # list of devices 92 | device_list = {} 93 | 94 | # get devices 95 | root = device_tree.getroot() 96 | for device in root.getchildren(): 97 | # get identifiers for device 98 | device_name = device.get("name") 99 | device_uuid = device.get("uuid") 100 | device_id = device.get("id") 101 | device_description = device.find("Description") 102 | 103 | # get the text value for the description 104 | if device_description is not None: 105 | device_description = device_description.text 106 | 107 | # create device 108 | new_device = MTDevice( 109 | device_id, device_name, device, device_uuid, device_description 110 | ) 111 | 112 | # get list of attributes 113 | for attribute in device.items(): 114 | new_device.add_attribute(attribute[0], attribute[1]) 115 | 116 | # get list of data items 117 | device_items = device.find("DataItems") 118 | if device_items is not None: 119 | process_dataitem(device_items, new_device, new_device) 120 | 121 | # get list of subcomponents 122 | component_item = device.find("Components") 123 | if component_item is not None: 124 | component_list = component_item.getchildren() 125 | process_components(component_list, new_device, new_device) 126 | 127 | device_list[new_device.id] = new_device 128 | 129 | return (device_list, device_tree) 130 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "astroid" 3 | version = "2.13.2" 4 | description = "An abstract syntax tree for Python with inference support." 5 | category = "dev" 6 | optional = false 7 | python-versions = ">=3.7.2" 8 | 9 | [package.dependencies] 10 | lazy-object-proxy = ">=1.4.0" 11 | typing-extensions = ">=4.0.0" 12 | wrapt = [ 13 | {version = ">=1.11,<2", markers = "python_version < \"3.11\""}, 14 | {version = ">=1.14,<2", markers = "python_version >= \"3.11\""}, 15 | ] 16 | 17 | [[package]] 18 | name = "black" 19 | version = "22.12.0" 20 | description = "The uncompromising code formatter." 21 | category = "dev" 22 | optional = false 23 | python-versions = ">=3.7" 24 | 25 | [package.dependencies] 26 | click = ">=8.0.0" 27 | mypy-extensions = ">=0.4.3" 28 | pathspec = ">=0.9.0" 29 | platformdirs = ">=2" 30 | tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} 31 | typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} 32 | 33 | [package.extras] 34 | colorama = ["colorama (>=0.4.3)"] 35 | d = ["aiohttp (>=3.7.4)"] 36 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 37 | uvloop = ["uvloop (>=0.15.2)"] 38 | 39 | [[package]] 40 | name = "click" 41 | version = "8.1.3" 42 | description = "Composable command line interface toolkit" 43 | category = "dev" 44 | optional = false 45 | python-versions = ">=3.7" 46 | 47 | [package.dependencies] 48 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 49 | 50 | [[package]] 51 | name = "colorama" 52 | version = "0.4.6" 53 | description = "Cross-platform colored terminal text." 54 | category = "dev" 55 | optional = false 56 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 57 | 58 | [[package]] 59 | name = "dill" 60 | version = "0.3.6" 61 | description = "serialize all of python" 62 | category = "dev" 63 | optional = false 64 | python-versions = ">=3.7" 65 | 66 | [package.extras] 67 | graph = ["objgraph (>=1.7.2)"] 68 | 69 | [[package]] 70 | name = "isort" 71 | version = "5.11.4" 72 | description = "A Python utility / library to sort Python imports." 73 | category = "dev" 74 | optional = false 75 | python-versions = ">=3.7.0" 76 | 77 | [package.extras] 78 | colors = ["colorama (>=0.4.3,<0.5.0)"] 79 | pipfile-deprecated-finder = ["pipreqs", "requirementslib"] 80 | plugins = ["setuptools"] 81 | requirements-deprecated-finder = ["pip-api", "pipreqs"] 82 | 83 | [[package]] 84 | name = "lazy-object-proxy" 85 | version = "1.9.0" 86 | description = "A fast and thorough lazy object proxy." 87 | category = "dev" 88 | optional = false 89 | python-versions = ">=3.7" 90 | 91 | [[package]] 92 | name = "mccabe" 93 | version = "0.7.0" 94 | description = "McCabe checker, plugin for flake8" 95 | category = "dev" 96 | optional = false 97 | python-versions = ">=3.6" 98 | 99 | [[package]] 100 | name = "mypy-extensions" 101 | version = "0.4.3" 102 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 103 | category = "dev" 104 | optional = false 105 | python-versions = "*" 106 | 107 | [[package]] 108 | name = "pathspec" 109 | version = "0.10.3" 110 | description = "Utility library for gitignore style pattern matching of file paths." 111 | category = "dev" 112 | optional = false 113 | python-versions = ">=3.7" 114 | 115 | [[package]] 116 | name = "platformdirs" 117 | version = "2.6.2" 118 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 119 | category = "dev" 120 | optional = false 121 | python-versions = ">=3.7" 122 | 123 | [package.extras] 124 | docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] 125 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] 126 | 127 | [[package]] 128 | name = "pylint" 129 | version = "2.15.10" 130 | description = "python code static checker" 131 | category = "dev" 132 | optional = false 133 | python-versions = ">=3.7.2" 134 | 135 | [package.dependencies] 136 | astroid = ">=2.12.13,<=2.14.0-dev0" 137 | colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} 138 | dill = [ 139 | {version = ">=0.2", markers = "python_version < \"3.11\""}, 140 | {version = ">=0.3.6", markers = "python_version >= \"3.11\""}, 141 | ] 142 | isort = ">=4.2.5,<6" 143 | mccabe = ">=0.6,<0.8" 144 | platformdirs = ">=2.2.0" 145 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 146 | tomlkit = ">=0.10.1" 147 | typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} 148 | 149 | [package.extras] 150 | spelling = ["pyenchant (>=3.2,<4.0)"] 151 | testutils = ["gitpython (>3)"] 152 | 153 | [[package]] 154 | name = "tomli" 155 | version = "2.0.1" 156 | description = "A lil' TOML parser" 157 | category = "dev" 158 | optional = false 159 | python-versions = ">=3.7" 160 | 161 | [[package]] 162 | name = "tomlkit" 163 | version = "0.11.6" 164 | description = "Style preserving TOML library" 165 | category = "dev" 166 | optional = false 167 | python-versions = ">=3.6" 168 | 169 | [[package]] 170 | name = "typing-extensions" 171 | version = "4.4.0" 172 | description = "Backported and Experimental Type Hints for Python 3.7+" 173 | category = "dev" 174 | optional = false 175 | python-versions = ">=3.7" 176 | 177 | [[package]] 178 | name = "wrapt" 179 | version = "1.14.1" 180 | description = "Module for decorators, wrappers and monkey patching." 181 | category = "dev" 182 | optional = false 183 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 184 | 185 | [metadata] 186 | lock-version = "1.1" 187 | python-versions = "^3.8" 188 | content-hash = "83cc54a015f40df14c5a1f1581ce4aac4eb10d071b5076b5ea6d5ec28e50fa23" 189 | 190 | [metadata.files] 191 | astroid = [ 192 | {file = "astroid-2.13.2-py3-none-any.whl", hash = "sha256:8f6a8d40c4ad161d6fc419545ae4b2f275ed86d1c989c97825772120842ee0d2"}, 193 | {file = "astroid-2.13.2.tar.gz", hash = "sha256:3bc7834720e1a24ca797fd785d77efb14f7a28ee8e635ef040b6e2d80ccb3303"}, 194 | ] 195 | black = [ 196 | {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"}, 197 | {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"}, 198 | {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"}, 199 | {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"}, 200 | {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"}, 201 | {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"}, 202 | {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"}, 203 | {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"}, 204 | {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"}, 205 | {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"}, 206 | {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"}, 207 | {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"}, 208 | ] 209 | click = [ 210 | {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, 211 | {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, 212 | ] 213 | colorama = [ 214 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 215 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 216 | ] 217 | dill = [ 218 | {file = "dill-0.3.6-py3-none-any.whl", hash = "sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0"}, 219 | {file = "dill-0.3.6.tar.gz", hash = "sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373"}, 220 | ] 221 | isort = [ 222 | {file = "isort-5.11.4-py3-none-any.whl", hash = "sha256:c033fd0edb91000a7f09527fe5c75321878f98322a77ddcc81adbd83724afb7b"}, 223 | {file = "isort-5.11.4.tar.gz", hash = "sha256:6db30c5ded9815d813932c04c2f85a360bcdd35fed496f4d8f35495ef0a261b6"}, 224 | ] 225 | lazy-object-proxy = [ 226 | {file = "lazy-object-proxy-1.9.0.tar.gz", hash = "sha256:659fb5809fa4629b8a1ac5106f669cfc7bef26fbb389dda53b3e010d1ac4ebae"}, 227 | {file = "lazy_object_proxy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b40387277b0ed2d0602b8293b94d7257e17d1479e257b4de114ea11a8cb7f2d7"}, 228 | {file = "lazy_object_proxy-1.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8c6cfb338b133fbdbc5cfaa10fe3c6aeea827db80c978dbd13bc9dd8526b7d4"}, 229 | {file = "lazy_object_proxy-1.9.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:721532711daa7db0d8b779b0bb0318fa87af1c10d7fe5e52ef30f8eff254d0cd"}, 230 | {file = "lazy_object_proxy-1.9.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:66a3de4a3ec06cd8af3f61b8e1ec67614fbb7c995d02fa224813cb7afefee701"}, 231 | {file = "lazy_object_proxy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1aa3de4088c89a1b69f8ec0dcc169aa725b0ff017899ac568fe44ddc1396df46"}, 232 | {file = "lazy_object_proxy-1.9.0-cp310-cp310-win32.whl", hash = "sha256:f0705c376533ed2a9e5e97aacdbfe04cecd71e0aa84c7c0595d02ef93b6e4455"}, 233 | {file = "lazy_object_proxy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea806fd4c37bf7e7ad82537b0757999264d5f70c45468447bb2b91afdbe73a6e"}, 234 | {file = "lazy_object_proxy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:946d27deaff6cf8452ed0dba83ba38839a87f4f7a9732e8f9fd4107b21e6ff07"}, 235 | {file = "lazy_object_proxy-1.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79a31b086e7e68b24b99b23d57723ef7e2c6d81ed21007b6281ebcd1688acb0a"}, 236 | {file = "lazy_object_proxy-1.9.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f699ac1c768270c9e384e4cbd268d6e67aebcfae6cd623b4d7c3bfde5a35db59"}, 237 | {file = "lazy_object_proxy-1.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfb38f9ffb53b942f2b5954e0f610f1e721ccebe9cce9025a38c8ccf4a5183a4"}, 238 | {file = "lazy_object_proxy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:189bbd5d41ae7a498397287c408617fe5c48633e7755287b21d741f7db2706a9"}, 239 | {file = "lazy_object_proxy-1.9.0-cp311-cp311-win32.whl", hash = "sha256:81fc4d08b062b535d95c9ea70dbe8a335c45c04029878e62d744bdced5141586"}, 240 | {file = "lazy_object_proxy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:f2457189d8257dd41ae9b434ba33298aec198e30adf2dcdaaa3a28b9994f6adb"}, 241 | {file = "lazy_object_proxy-1.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d9e25ef10a39e8afe59a5c348a4dbf29b4868ab76269f81ce1674494e2565a6e"}, 242 | {file = "lazy_object_proxy-1.9.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cbf9b082426036e19c6924a9ce90c740a9861e2bdc27a4834fd0a910742ac1e8"}, 243 | {file = "lazy_object_proxy-1.9.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f5fa4a61ce2438267163891961cfd5e32ec97a2c444e5b842d574251ade27d2"}, 244 | {file = "lazy_object_proxy-1.9.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8fa02eaab317b1e9e03f69aab1f91e120e7899b392c4fc19807a8278a07a97e8"}, 245 | {file = "lazy_object_proxy-1.9.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e7c21c95cae3c05c14aafffe2865bbd5e377cfc1348c4f7751d9dc9a48ca4bda"}, 246 | {file = "lazy_object_proxy-1.9.0-cp37-cp37m-win32.whl", hash = "sha256:f12ad7126ae0c98d601a7ee504c1122bcef553d1d5e0c3bfa77b16b3968d2734"}, 247 | {file = "lazy_object_proxy-1.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:edd20c5a55acb67c7ed471fa2b5fb66cb17f61430b7a6b9c3b4a1e40293b1671"}, 248 | {file = "lazy_object_proxy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d0daa332786cf3bb49e10dc6a17a52f6a8f9601b4cf5c295a4f85854d61de63"}, 249 | {file = "lazy_object_proxy-1.9.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cd077f3d04a58e83d04b20e334f678c2b0ff9879b9375ed107d5d07ff160171"}, 250 | {file = "lazy_object_proxy-1.9.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:660c94ea760b3ce47d1855a30984c78327500493d396eac4dfd8bd82041b22be"}, 251 | {file = "lazy_object_proxy-1.9.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:212774e4dfa851e74d393a2370871e174d7ff0ebc980907723bb67d25c8a7c30"}, 252 | {file = "lazy_object_proxy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f0117049dd1d5635bbff65444496c90e0baa48ea405125c088e93d9cf4525b11"}, 253 | {file = "lazy_object_proxy-1.9.0-cp38-cp38-win32.whl", hash = "sha256:0a891e4e41b54fd5b8313b96399f8b0e173bbbfc03c7631f01efbe29bb0bcf82"}, 254 | {file = "lazy_object_proxy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:9990d8e71b9f6488e91ad25f322898c136b008d87bf852ff65391b004da5e17b"}, 255 | {file = "lazy_object_proxy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9e7551208b2aded9c1447453ee366f1c4070602b3d932ace044715d89666899b"}, 256 | {file = "lazy_object_proxy-1.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f83ac4d83ef0ab017683d715ed356e30dd48a93746309c8f3517e1287523ef4"}, 257 | {file = "lazy_object_proxy-1.9.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7322c3d6f1766d4ef1e51a465f47955f1e8123caee67dd641e67d539a534d006"}, 258 | {file = "lazy_object_proxy-1.9.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:18b78ec83edbbeb69efdc0e9c1cb41a3b1b1ed11ddd8ded602464c3fc6020494"}, 259 | {file = "lazy_object_proxy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:09763491ce220c0299688940f8dc2c5d05fd1f45af1e42e636b2e8b2303e4382"}, 260 | {file = "lazy_object_proxy-1.9.0-cp39-cp39-win32.whl", hash = "sha256:9090d8e53235aa280fc9239a86ae3ea8ac58eff66a705fa6aa2ec4968b95c821"}, 261 | {file = "lazy_object_proxy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f"}, 262 | ] 263 | mccabe = [ 264 | {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, 265 | {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, 266 | ] 267 | mypy-extensions = [ 268 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 269 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 270 | ] 271 | pathspec = [ 272 | {file = "pathspec-0.10.3-py3-none-any.whl", hash = "sha256:3c95343af8b756205e2aba76e843ba9520a24dd84f68c22b9f93251507509dd6"}, 273 | {file = "pathspec-0.10.3.tar.gz", hash = "sha256:56200de4077d9d0791465aa9095a01d421861e405b5096955051deefd697d6f6"}, 274 | ] 275 | platformdirs = [ 276 | {file = "platformdirs-2.6.2-py3-none-any.whl", hash = "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490"}, 277 | {file = "platformdirs-2.6.2.tar.gz", hash = "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2"}, 278 | ] 279 | pylint = [ 280 | {file = "pylint-2.15.10-py3-none-any.whl", hash = "sha256:9df0d07e8948a1c3ffa3b6e2d7e6e63d9fb457c5da5b961ed63106594780cc7e"}, 281 | {file = "pylint-2.15.10.tar.gz", hash = "sha256:b3dc5ef7d33858f297ac0d06cc73862f01e4f2e74025ec3eff347ce0bc60baf5"}, 282 | ] 283 | tomli = [ 284 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 285 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 286 | ] 287 | tomlkit = [ 288 | {file = "tomlkit-0.11.6-py3-none-any.whl", hash = "sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b"}, 289 | {file = "tomlkit-0.11.6.tar.gz", hash = "sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73"}, 290 | ] 291 | typing-extensions = [ 292 | {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, 293 | {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, 294 | ] 295 | wrapt = [ 296 | {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, 297 | {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"}, 298 | {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28"}, 299 | {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59"}, 300 | {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87"}, 301 | {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1"}, 302 | {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b"}, 303 | {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462"}, 304 | {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1"}, 305 | {file = "wrapt-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320"}, 306 | {file = "wrapt-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2"}, 307 | {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4"}, 308 | {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069"}, 309 | {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310"}, 310 | {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f"}, 311 | {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656"}, 312 | {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c"}, 313 | {file = "wrapt-1.14.1-cp310-cp310-win32.whl", hash = "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8"}, 314 | {file = "wrapt-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164"}, 315 | {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907"}, 316 | {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3"}, 317 | {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3"}, 318 | {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d"}, 319 | {file = "wrapt-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7"}, 320 | {file = "wrapt-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00"}, 321 | {file = "wrapt-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4"}, 322 | {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1"}, 323 | {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1"}, 324 | {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff"}, 325 | {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d"}, 326 | {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1"}, 327 | {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569"}, 328 | {file = "wrapt-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed"}, 329 | {file = "wrapt-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471"}, 330 | {file = "wrapt-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248"}, 331 | {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68"}, 332 | {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d"}, 333 | {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77"}, 334 | {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7"}, 335 | {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015"}, 336 | {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a"}, 337 | {file = "wrapt-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853"}, 338 | {file = "wrapt-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c"}, 339 | {file = "wrapt-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456"}, 340 | {file = "wrapt-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f"}, 341 | {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc"}, 342 | {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1"}, 343 | {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"}, 344 | {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b"}, 345 | {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0"}, 346 | {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57"}, 347 | {file = "wrapt-1.14.1-cp38-cp38-win32.whl", hash = "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5"}, 348 | {file = "wrapt-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d"}, 349 | {file = "wrapt-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383"}, 350 | {file = "wrapt-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7"}, 351 | {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86"}, 352 | {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735"}, 353 | {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b"}, 354 | {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3"}, 355 | {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3"}, 356 | {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe"}, 357 | {file = "wrapt-1.14.1-cp39-cp39-win32.whl", hash = "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5"}, 358 | {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"}, 359 | {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, 360 | ] 361 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "MTConnect" 3 | version = "0.3.3" 4 | description = "A python agent for MTConnect" 5 | authors = ["Michael Honaker "] 6 | license = "\"Apache License 2.0\"" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.8" 10 | 11 | [tool.poetry.dev-dependencies] 12 | black = "^22.12.0" 13 | pylint = "^2.15.10" 14 | 15 | [build-system] 16 | requires = ["poetry-core>=1.0.0"] 17 | build-backend = "poetry.core.masonry.api" 18 | -------------------------------------------------------------------------------- /scripts/format_package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | python3 -m black . "$@" -------------------------------------------------------------------------------- /scripts/lint_package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | python3 -m pylint mtconnect tests "$@" -------------------------------------------------------------------------------- /scripts/publish_package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | poetry build 4 | poetry publish --username "${PYPI_USER}" --password "${PYPI_PASSWORD}" -------------------------------------------------------------------------------- /scripts/run_container.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "$#" -lt 2 ]; then 4 | echo "Please supply image name and command to run" 5 | exit 1 6 | fi 7 | 8 | image_name=$1 9 | 10 | docker run --name run-ctr --rm ${image_name} "${@:2}" -------------------------------------------------------------------------------- /scripts/run_develop_container.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "$#" -lt 2 ]; then 4 | echo "Please supply image name and command to run" 5 | exit 1 6 | fi 7 | 8 | image_name=$1 9 | 10 | docker run -it --name develop-ctr \ 11 | -e PYPI_USER="${PYPI_USER}" \ 12 | -e PYPI_PASSWORD="${PYPI_PASSWORD}" \ 13 | --rm ${image_name} "${@:2}" -------------------------------------------------------------------------------- /scripts/set_version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "$#" -ne 1 ]; then 4 | echo "Please supply version" 5 | exit 1 6 | fi 7 | 8 | poetry version "$@" -------------------------------------------------------------------------------- /scripts/test_package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | python3 -m unittest "$@" -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HonakerM/MTConnect-Python-Agent/a503b3e99e204490ae296c3223c737d499f343da/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_agent.py: -------------------------------------------------------------------------------- 1 | # import user test 2 | import unittest 3 | 4 | # import storage objects 5 | from mtconnect.agent import MTConnect 6 | 7 | # import libraries for testing 8 | import os 9 | import datetime 10 | 11 | 12 | class AgentTest(unittest.TestCase): 13 | test_agent = None 14 | 15 | def __init__(self, *args, **kwargs): 16 | super(AgentTest, self).__init__(*args, **kwargs) 17 | self.test_agent = MTConnect(loc="tests/test_device.xml") 18 | 19 | def testProbe(self): 20 | self.test_agent.probe() 21 | 22 | def testCurrent(self): 23 | self.test_agent.current() 24 | 25 | def testSample(self): 26 | self.test_agent.sample() 27 | 28 | def testSetDeviceVariables(self): 29 | device = self.test_agent.get_device() 30 | # test update 31 | self.test_agent.set_device_name(device, "test_name") 32 | new_name = device.xml_data.get("name") 33 | self.assertEqual(new_name, "test_name") 34 | 35 | self.test_agent.set_device_id(device, 2) 36 | new_id = device.xml_data.get("id") 37 | self.assertEqual(new_id, 2) 38 | 39 | 40 | if __name__ == "__main__": 41 | unittest.main() 42 | -------------------------------------------------------------------------------- /tests/test_device.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Mazak FCA751PY-N08 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 | -------------------------------------------------------------------------------- /tests/test_storage.py: -------------------------------------------------------------------------------- 1 | # import user test 2 | import unittest 3 | 4 | # import storage objects 5 | from mtconnect.storage import MTBuffer, MTDataEntity 6 | from mtconnect.device import MTDataItem, MTDevice 7 | 8 | # import libraries for testing 9 | import os 10 | import datetime 11 | 12 | 13 | class DataEntityTest(unittest.TestCase): 14 | test_device = None 15 | test_item_1 = None 16 | 17 | def __init__(self, *args, **kwargs): 18 | super(DataEntityTest, self).__init__(*args, **kwargs) 19 | 20 | self.test_device = MTDevice("test_device", "1", None) 21 | self.test_item_1 = MTDataItem( 22 | "test_1", "test_1", "SYSTEM", "SAMPLE", self.test_device, self.test_device 23 | ) 24 | 25 | def testDataCreation(self): 26 | entity = MTDataEntity(self.test_item_1, 1) 27 | 28 | self.assertTrue(isinstance(entity.timestamp, type(datetime.datetime.now()))) 29 | self.assertEqual(entity.dataItem.id, "test_1") 30 | self.assertEqual(entity.value, 1) 31 | 32 | def testSequenceNumber(self): 33 | entity = MTDataEntity(self.test_item_1, 1) 34 | entity.set_sequence(1) 35 | self.assertEqual(entity.sequence_number, 1) 36 | 37 | 38 | class BufferTest(unittest.TestCase): 39 | test_device = None 40 | test_item_1 = None 41 | test_item_2 = None 42 | 43 | test_buffer = None 44 | 45 | def __init__(self, *args, **kwargs): 46 | super(BufferTest, self).__init__(*args, **kwargs) 47 | 48 | self.test_device = MTDevice("test_device", "1", None) 49 | self.test_item_1 = MTDataItem( 50 | "test_1", "test_1", "SYSTEM", "SAMPLE", self.test_device, self.test_device 51 | ) 52 | self.test_item_2 = MTDataItem( 53 | "test_2", "test_2", "SYSTEM", "SAMPLE", self.test_device, self.test_device 54 | ) 55 | 56 | self.test_buffer = MTBuffer(buffer_length=2) 57 | 58 | def testBufferCreation(self): 59 | buffer = MTBuffer() 60 | self.assertEqual(len(buffer.get_buffer()), 16384) 61 | os.environ["BUFFER_SIZE"] = "10" 62 | buffer = MTBuffer() 63 | self.assertEqual(len(buffer.get_buffer()), 10) 64 | buffer = MTBuffer(buffer_length=2) 65 | self.assertEqual(len(buffer.get_buffer()), 2) 66 | 67 | def testBufferPush(self): 68 | # initalize buffer 69 | buffer = self.test_buffer 70 | 71 | data_1 = MTDataEntity(self.test_item_1, 1) 72 | data_2 = MTDataEntity(self.test_item_1, 2) 73 | data_3 = MTDataEntity(self.test_item_2, 1) 74 | 75 | buffer.push(data_1) 76 | self.assertEqual(data_1.sequence_number, 1) 77 | self.assertEqual(buffer.get_buffer(), [data_1, None]) 78 | self.assertEqual(buffer.first_sequence, 1) 79 | 80 | buffer.push(data_2) 81 | self.assertEqual(data_2.sequence_number, 2) 82 | self.assertEqual(buffer.get_buffer(), [data_1, data_2]) 83 | self.assertEqual(buffer.last_sequence, 2) 84 | self.assertEqual(buffer.first_sequence, 1) 85 | 86 | buffer.push(data_3) 87 | self.assertEqual(data_3.sequence_number, 3) 88 | self.assertEqual(buffer.get_buffer(), [data_2, data_3]) 89 | self.assertEqual(buffer.last_sequence, 3) 90 | self.assertEqual(buffer.first_sequence, 2) 91 | 92 | def testBufferGet(self): 93 | buffer = self.test_buffer 94 | 95 | test_device = MTDevice("test_device", "1", "1", None) 96 | test_item = MTDataItem( 97 | "test_1", "test_1", "SYSTEM", "SAMPLE", test_device, test_device 98 | ) 99 | data_1 = MTDataEntity(test_item, 1) 100 | data_2 = MTDataEntity(test_item, 2) 101 | buffer.push(data_1) 102 | buffer.push(data_2) 103 | 104 | self.assertEqual(buffer.get_data(1, 1), ([data_1], 2)) 105 | self.assertEqual(buffer.get_data(1, 2), ([data_1, data_2], 3)) 106 | self.assertEqual(buffer.get_data(1, 3), ([data_1, data_2], 3)) 107 | 108 | 109 | if __name__ == "__main__": 110 | unittest.main() 111 | -------------------------------------------------------------------------------- /tests/test_xml.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 2260 7 | 8 | 9 | 10 | 11 | 12 | 13 | 1853 14 | 15 | 16 | 17 | 18 | 19 | 20 | --------------------------------------------------------------------------------