├── .codeclimate.yml ├── .gitattributes ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── dev-requirements.txt ├── devicecloud ├── __init__.py ├── apibase.py ├── conditions.py ├── data │ ├── __init__.py │ └── devicecloud.crt ├── devicecore.py ├── examples │ ├── __init__.py │ ├── cookbook_streams.py │ ├── devicecore_playground.py │ ├── example_helpers.py │ ├── file_system_service_playground.py │ ├── filedata_playground.py │ ├── monitor_playground.py │ └── streams_playground.py ├── file_system_service.py ├── filedata.py ├── monitor.py ├── monitor_tcp.py ├── sci.py ├── streams.py ├── test │ ├── __init__.py │ ├── integration │ │ ├── __init__.py │ │ ├── inttest_monitor_tcp.py │ │ ├── inttest_streams.py │ │ └── inttest_utilities.py │ └── unit │ │ ├── __init__.py │ │ ├── test_conditions.py │ │ ├── test_core.py │ │ ├── test_devicecore.py │ │ ├── test_file_system_service.py │ │ ├── test_filedata.py │ │ ├── test_monitor.py │ │ ├── test_monitor_tcp.py │ │ ├── test_sci.py │ │ ├── test_streams.py │ │ ├── test_utilities.py │ │ ├── test_version.py │ │ └── test_ws.py ├── util.py ├── version.py └── ws.py ├── docs ├── Makefile ├── _static │ └── .keepme ├── conf.py ├── cookbook.rst ├── core.rst ├── devicecore.rst ├── filedata.rst ├── filesystem.rst ├── index.rst ├── make.bat ├── monitor.rst ├── sci.rst ├── streams.rst └── ws.rst ├── inttest.sh ├── make.bat ├── make.sh ├── requirements.txt ├── setup.py ├── test-requirements.txt ├── tox.ini └── toxtest.sh /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | # 2 | # ---Choose Your Languages--- 3 | # To disable analysis for a certain language, set the language to `false`. 4 | # For help setting your languages: 5 | # http://docs.codeclimate.com/article/169-configuring-analysis-languages 6 | # 7 | languages: 8 | Ruby: false 9 | Javascript: false 10 | PHP: false 11 | Python: true 12 | 13 | # 14 | # ---Exclude Files or Directories--- 15 | # List the files or directories you would like excluded from analysis. 16 | # For help setting your exclude paths: 17 | # http://docs.codeclimate.com/article/166-excluding-files-folders 18 | # 19 | exclude_paths: 20 | - devicecloud/test/** 21 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=auto 3 | 4 | # Explicitly declare text files you want to always be normalized and converted 5 | # to native line endings on checkout. 6 | *.c text 7 | *.h text 8 | 9 | # Declare files that will always have LF line endings on checkout. 10 | *.sh text eol=lf 11 | 12 | # Denote all files that are truly binary and should not be modified. 13 | *.png binary 14 | *.jpg binary 15 | 16 | # Existing attributes 17 | *.java diff 18 | *.js diff 19 | *.pl diff 20 | *.txt diff 21 | *.ts diff 22 | *.html diff 23 | *.sh diff 24 | *.xml diff 25 | *.py diff 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------- 2 | # Python Binary 3 | #------------------------------------------------------------------------------- 4 | *.pyc 5 | *.pyo 6 | 7 | #------------------------------------------------------------------------------- 8 | # Editor file trash 9 | #------------------------------------------------------------------------------- 10 | *~ 11 | \#*\# 12 | 13 | #------------------------------------------------------------------------------- 14 | # Virtualenv 15 | #------------------------------------------------------------------------------- 16 | env/ 17 | env3/ 18 | .env/ 19 | .env3/ 20 | .toxenv/ 21 | venv/ 22 | .venv/ 23 | .venv2/ 24 | 25 | #------------------------------------------------------------------------------- 26 | # Setuptools Stuff 27 | #------------------------------------------------------------------------------- 28 | dist/ 29 | *.egg-info 30 | 31 | #------------------------------------------------------------------------------- 32 | # Installer logs 33 | #------------------------------------------------------------------------------- 34 | pip-log.txt 35 | 36 | #------------------------------------------------------------------------------- 37 | # Unit test and coverage reports 38 | #------------------------------------------------------------------------------- 39 | cover 40 | .coverage 41 | .tox 42 | 43 | #------------------------------------------------------------------------------- 44 | # CPython Extensions and Build Artifacts 45 | #------------------------------------------------------------------------------- 46 | *.o 47 | *.d 48 | *.a 49 | *.so 50 | *.d 51 | *.o 52 | *.a 53 | 54 | #------------------------------------------------------------------------------- 55 | # IDE settings 56 | #------------------------------------------------------------------------------- 57 | .idea/ 58 | .pydevproject 59 | .project 60 | 61 | #------------------------------------------------------------------------------- 62 | # Sphinx build files 63 | #------------------------------------------------------------------------------- 64 | docs/_build/ 65 | 66 | #------------------------------------------------------------------------------- 67 | # Application builds 68 | #------------------------------------------------------------------------------- 69 | build/ 70 | 71 | #------------------------------------------------------------------------------- 72 | # Temporary files 73 | #------------------------------------------------------------------------------- 74 | tmp/ 75 | 76 | #------------------------------------------------------------------------------- 77 | # Generated Files 78 | #------------------------------------------------------------------------------- 79 | README.rst 80 | *.csv 81 | *.log 82 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # 2 | # This is the Travis-CI configuration. 3 | # 4 | # The actual dependency installation and test execution is done via tox as a 5 | # way to share the same process between Travis-CI and Buildbot. 6 | # 7 | language: python 8 | # allow travis to use new, faster container based 9 | # infrastructure to perform the testing 10 | sudo: false 11 | 12 | # this version of python is only used to run tox - the version specified by TOX_ENV 13 | # is used to install and run tests 14 | python: 2.7 15 | env: 16 | - TOX_ENV=py27 17 | - TOX_ENV=py34 18 | - TOX_ENV=coverage 19 | 20 | # command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors 21 | install: 22 | - pip install tox-travis coveralls 23 | - pip install -r test-requirements.txt 24 | 25 | # command to run tests, e.g. python setup.py test 26 | script: 27 | - tox 28 | 29 | after_success: 30 | - coveralls 31 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Python Devicecloud Library Changelog 2 | 3 | 4 | ### 0.5.9 / 2021-02-18 5 | [Full Changelog](https://github.com/digidotcom/python-devicecloud/compare/0.5.8...0.5.9) 6 | 7 | Enhancement: 8 | 9 | * core: update other dependecies to "compatible" versions on install 10 | 11 | 12 | ### 0.5.8 / 2021-02-17 13 | [Full Changelog](https://github.com/digidotcom/python-devicecloud/compare/0.5.7...0.5.8) 14 | 15 | Enhancement: 16 | 17 | * datastreams: add "GEOJSON" format type to DataStreams 18 | 19 | 20 | ### 0.5.7 / 2020-04-22 21 | [Full Changelog](https://github.com/digidotcom/python-devicecloud/compare/0.5.6...0.5.7) 22 | 23 | Enhancement: 24 | 25 | * update requests dependency to 2.20 26 | * update other dependecies to "compatible" versions on install 27 | 28 | 29 | ### 0.5.6 / 2019-07-24 30 | [Full Changelog](https://github.com/digidotcom/python-devicecloud/compare/0.5.5...0.5.6) 31 | 32 | Enhancement: 33 | 34 | * core: update package long description 35 | 36 | ### 0.5.5 / 2019-07-23 37 | [Full Changelog](https://github.com/digidotcom/python-devicecloud/compare/0.5.4...0.5.5) 38 | 39 | Enhancement: 40 | 41 | * core: remove subprocess during installation (make installation Windows compatible) 42 | 43 | 44 | ### 0.5.4 / 2019-05-22 45 | [Full Changelog](https://github.com/digidotcom/python-devicecloud/compare/0.5.3...0.5.4) 46 | 47 | Enhancement: 48 | 49 | * tests: added test for sci firmware update attributes 50 | 51 | 52 | ### 0.5.3 / 2018-07-24 53 | [Full Changelog](https://github.com/digidotcom/python-devicecloud/compare/0.5.1...0.5.3) 54 | 55 | Enhancements: 56 | 57 | * devicecore: add support for adding multiple tags 58 | 59 | 60 | ### 0.5.1 / 2018-07-20 61 | [Full Changelog](https://github.com/digidotcom/python-devicecloud/compare/0.5.0...0.5.1) 62 | 63 | Changes: 64 | 65 | * pypi: upgrade Development Status to "4 - Beta" 66 | 67 | 68 | ### 0.5.0 / 2018-07-20 69 | [Full Changelog](https://github.com/digidotcom/python-devicecloud/compare/0.4.3...0.5.0) 70 | 71 | Enhancements: 72 | 73 | * devicecore: add support for adding and removing tags from a device 74 | 75 | 76 | ### 0.4.3 / 2018-06-11 77 | [Full Changelog](https://github.com/digidotcom/python-devicecloud/compare/0.4.2...0.4.3) 78 | 79 | Bug Fixes: 80 | 81 | * core: updated Device Cloud CRT to latest 82 | * core: updated requirements to latest 83 | 84 | ### 0.4.2 / 2015-11-20 85 | [Full Changelog](https://github.com/digidotcom/python-devicecloud/compare/0.4.1...0.4.2) 86 | 87 | Bug Fixes: 88 | 89 | * core: All Etherios references have been replaced with Digi. Most 90 | importantly, the default URL for the devicecloud is now 91 | devicecloud.digi.com. The old URL may not redirect properly at some 92 | point in the future. 93 | 94 | ### 0.4.1 / 2015-11-13 95 | [Full Changelog](https://github.com/digidotcom/python-devicecloud/compare/0.4...0.4.1) 96 | 97 | Bug Fixes: 98 | 99 | * sci: Targetting groups now works 100 | 101 | ### 0.4 / 2015-10-01 102 | [Full Changelog](https://github.com/digidotcom/python-devicecloud/compare/0.3...0.4) 103 | 104 | Enhancements: 105 | 106 | * monitors: basic support for creating HTTP monitors was added 107 | * streams: support for the JSON data type added 108 | * sci: support added for filesystem service added. This allows you to 109 | access files and directories on any device supporting this service. 110 | 111 | Bug Fixes: 112 | 113 | * streams: fix data translations from device cloud <-> python types 114 | when reading and writing data points. See 115 | https://github.com/digidotcom/python-devicecloud/commit/5ac6c15cddf010709361c16b69e622aca93d6b28 116 | 117 | ### 0.3 / 2015-06-15 118 | [Full Changelog](https://github.com/digidotcom/python-devicecloud/compare/0.2...0.3) 119 | 120 | Enhancements: 121 | 122 | * monitor: Support for the Monitor API was added allowing for 123 | querying, adding, and removing of monitors (limiited to TCP right 124 | now). 125 | * monitor: Support for listening for changes and receiving callbacks 126 | when monitors are triggered has been added. Support is limited to 127 | SSL/TCP for now. 128 | * streams: get_stream() now optionally accepts a ``stream_prefix`` 129 | that allow for restricting which streams are returned based on path. 130 | * sci: support added for ``get_job_async`` 131 | * devicecore: provisioning: support added for removing devices from an 132 | account. 133 | * core: HTTP sessions are now used in order to allow for HTTP 134 | connection reuse and credential reuse (via cookies) 135 | 136 | Bug Fixes: 137 | 138 | * Unit tests for fixed for Python 3.4 139 | * filedata: ``raw`` option added to work around some issues when 140 | retrieving binary data as base64 141 | 142 | ### 0.2 / 2015-01-23 143 | [Full Changelog](https://github.com/digidotcom/python-devicecloud/compare/0.1.1...0.2) 144 | 145 | Enhancements: 146 | 147 | * Integration tests were added for some APIs to further test library correctness 148 | * Documentation for several parts of the system, including streams 149 | was enhanced. 150 | * Several APIs now support pagination on large result sets that previously lacked support. 151 | * We now expose the underlying object used to send device cloud API requests via 152 | `dc.get_connection()`. This allows users to more easily talk to currently unsupported 153 | APIs. Support for the basic API verbs plus helpers for handling pagination and other 154 | concerns are included. 155 | * Support for group operations in devicecore is now present 156 | * Filedata files and directories can now be deleted 157 | * A new 'ws' API has been added for making direct web service requests 158 | * DeviceCore now has support for provisioning one or multiple devices 159 | 160 | API Changes: 161 | 162 | * devicecore: `list_devices` was changed to `get_devices`. Previously, the 163 | code did not match the documentation on this front. Calls to `dc.devicecore.list_devices` 164 | will need to be changed to `dc.devicecore.get_devices` 165 | 166 | Bug Fixes: 167 | 168 | * streams: When creating a stream, the stream_id was used for the description rather 169 | than the provided description. 170 | * core: Library now freezes dependencies. Several dependencies updated to latest 171 | versions (e.g. requests). Without this, some combinations of the library 172 | and dependencies caused various errors. 173 | 174 | Thanks to Dan Harrison, Steve Stack, Tom Manley, and Paul Osborne for contributions 175 | going into this release. 176 | 177 | ### 0.1.1 / 2014-09-14 178 | [Full Changelog](https://github.com/digidotcom/python-devicecloud/compare/0.1...0.1.1) 179 | 180 | Enhancements: 181 | 182 | * Additional badges were added to the main README 183 | * Documentation updates and improvements 184 | * Packaging was improved to not valid DRY with version 185 | 186 | Bug Fixes: 187 | 188 | * PYTHONDC-90: Restructured text is now included with sdist releases. This removes 189 | an unsightly warning when installing the package. 190 | * PYTHONDC-91: Mismatches between the documentation and code were fixed. 191 | 192 | ### 0.1 / 2014-09-12 193 | The initial version of the library was released into the world. This version 194 | supported the following features: 195 | 196 | * Getting basic device information via DeviceCore 197 | * Listing devices associated with a device cloud account 198 | * Interacting with Device Cloud Data Streams 199 | * Create Streams 200 | * Get Stream (by id) 201 | * List all streams 202 | * Get metadata for a stream 203 | * Write a single datapoint to a stream 204 | * Write many datapoints to a stream (homogeneous bulk write) 205 | * Write many datapoints to multiple streams (heterogeneous bulk write) 206 | * Read data points from a stream (includes control over order of 207 | returned data set as well as allowing for retrieving data 208 | roll-ups, etc.) 209 | * Support for accessing Device Cloud FileData store 210 | * Get filedata matching a provided condition (path, file extension, 211 | size, etc.) 212 | * Write files to filedata store 213 | * Recursively walk filedata directory tree from some root location 214 | * Get full metadata and contents of files and directories. 215 | * Low level support for performing basic SCI commands with limited parsing 216 | of results and support for only a subset of available services/commands. 217 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Developer's Guide 2 | ================= 3 | 4 | Environment Setup 5 | ----------------- 6 | 7 | All the requirements in order to perform development on the product 8 | should be installable in a virtualenv. 9 | 10 | $ pip install -r dev-requirements.txt 11 | 12 | In order to build a release you will also need to install pandoc. On 13 | Ubuntu, you should be able to do: 14 | 15 | $ sudo apt-get install pandoc 16 | 17 | 18 | Running the Unit Tests 19 | ---------------------- 20 | 21 | ### Running Tests with Nose 22 | 23 | Running the tests is easy with nose (included in 24 | test-requirements.txt). From the project root: 25 | 26 | $ nosetests . 27 | 28 | To run the unit tests with coverage results (view cover/index.html), 29 | do the following: 30 | 31 | $ nosetests --with-coverage --cover-html --cover-package=devicecloud . 32 | 33 | New contributions to the library will only be accepted if they include 34 | unit test coverage (with some exceptions). 35 | 36 | ### Testing All Versions with Tox 37 | 38 | We also support running the tests against all supported versions of 39 | python using a combination of 40 | [tox](http://tox.readthedocs.org/en/latest/) and 41 | [pyenv](https://github.com/yyuu/pyenv). To run all of the tests 42 | against all supported versions of python, just do the following: 43 | 44 | $ ./toxtest.sh 45 | 46 | This might take awhile the first time as it will build from source a 47 | version of the interpreter for each version supported. If you recieve 48 | errors from pyenv, there may be addition dependencies required. 49 | Please visit https://github.com/yyuu/pyenv/wiki/Common-build-problems 50 | for additional pointers. 51 | 52 | ### Running Integration and Unittests 53 | 54 | There are some additional integration tests that run against an actual 55 | device cloud account. These are a bit more fragile and when something 56 | fails, you may need to go to your device cloud account to clean things 57 | up. 58 | 59 | To run those tests, you can just do the following. This script runs 60 | the toxtest.sh script with environment variables set with your 61 | account information. The tests that were skipped before will now 62 | be run with each supported version of the interpreter: 63 | 64 | $ ./inttest.sh 65 | 66 | Build the Documentation 67 | ----------------------- 68 | 69 | Documentation (outside of this file and the README) is done via 70 | Sphinx. To build the docs, just do the following (with virtualenv 71 | activated): 72 | 73 | $ cd docs 74 | $ make html 75 | 76 | The docs that are built will be located at 77 | docs/_build/html/index.html. 78 | 79 | The documentation for the project is published on github using a [Github 80 | Pages](https://pages.github.com/) Project Site. The process for 81 | releasing a new set of documentation is the following: 82 | 83 | 1. Create a fresh clone of the project and checkout the `gh-pages` 84 | branch. Although this is the same repo, the tree is completely 85 | separate from the main python-devicecloud codebase. 86 | 2. Remove all contents from the working area 87 | 3. From the python-devicecloud repo, `cp -r docs/_build/html/* 88 | /path/to/other/repo/`. 89 | 4. Commit and push the update `gh-pages` branch to github 90 | 91 | Open Source License Header 92 | -------------------------- 93 | 94 | Each source file should be prefixed with the following header: 95 | 96 | # This Source Code Form is subject to the terms of the Mozilla Public 97 | # License, v. 2.0. If a copy of the MPL was not distributed with this 98 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 99 | # 100 | # Copyright (c) 2015-2018 Digi International Inc. All rights reserved. 101 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include README.rst 3 | include requirements.txt 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Python Device Cloud Library 2 | =========================== 3 | 4 | [![Build Status](https://travis-ci.org/digidotcom/python-devicecloud.svg?branch=master)](https://travis-ci.org/digidotcom/python-devicecloud) 5 | [![Coverage Status](https://img.shields.io/coveralls/digidotcom/python-devicecloud.svg)](https://coveralls.io/r/digidotcom/python-devicecloud) 6 | [![Latest Version](https://img.shields.io/pypi/v/devicecloud.svg)](https://pypi.python.org/pypi/devicecloud/) 7 | [![License](https://img.shields.io/badge/license-MPL%202.0-blue.svg)](https://github.com/digidotcom/python-devicecloud/blob/master/LICENSE) 8 | 9 | Be sure to check out the [full documentation](https://digidotcom.github.io/python-devicecloud). A [Changelog](https://github.com/digidotcom/python-devicecloud/blob/master/CHANGELOG.md) is also available. 10 | 11 | Overview 12 | -------- 13 | 14 | Python-devicecloud is a library providing simple, intuitive access to [Digi Device Cloud(sm)](https://www.digi.com/products/cloud/digi-device-cloud) for clients written in Python. 15 | 16 | The library wraps Device Cloud's REST API and hides the details of forming HTTP requests in order to gain access to device information, file data, streams, and other features of Device Cloud. The API can be found [here](https://ftp1.digi.com/support/documentation/90002008_redirect.htm). 17 | 18 | The primary target audience for this library is individuals interfacing with Device Cloud from the server side or developers writing tools to aid device development. For efficient connectivity from devices, we suggest that you first look at using the [Device Cloud Connector](https://www.digi.com/support/productdetail?pid=5575). That being said, this library could also be used on devices if deemed suitable. 19 | 20 | Example 21 | ------- 22 | 23 | The library provides access to a wide array of features, but here is a couple quick examples to give you a taste of what the API looks like. 24 | 25 | ```python 26 | from devicecloud import DeviceCloud 27 | 28 | dc = DeviceCloud('user', 'pass') 29 | 30 | # show the MAC address of all devices that are currently connected 31 | # 32 | # This is done using Device Cloud DeviceCore functionality 33 | print "== Connected Devices ==" 34 | for device in dc.devicecore.get_devices(): 35 | if device.is_connected(): 36 | print device.get_mac() 37 | 38 | # get the name and current value of all data streams having values 39 | # with a floating point type 40 | # 41 | # This is done using Device Cloud stream functionality 42 | for stream in dc.streams.get_streams(): 43 | if stream.get_data_type().lower() in ('float', 'double'): 44 | print "%s -> %s" % (stream.get_stream_id(), stream.get_current_value()) 45 | ``` 46 | 47 | For more examples and detailed documentation, be sure to checkout out the [Full API Documentation](https://digidotcom.github.io/python-devicecloud). 48 | 49 | Installation 50 | ------------ 51 | 52 | This library can be installed using [pip](https://github.com/pypa/pip). Python versions 2.7+ and 3+ are supported by the library. 53 | 54 | ```sh 55 | pip install devicecloud 56 | ``` 57 | 58 | If you already have an older version of the library installed, you can upgrade to the latest version by doing 59 | 60 | ```sh 61 | pip install --upgrade devicecloud 62 | ``` 63 | 64 | Supported Features 65 | ------------------ 66 | 67 | Eventually, it is hoped that there will be complete feature parity between Device Cloud API and this library. For now, however, that is not the case. The current features are supported by the library: 68 | 69 | * Getting basic device information via DeviceCore 70 | * Provision and Delete devices via DeviceCore 71 | * Listing devices associated with a device cloud account 72 | * Interacting with Device Cloud Data Streams 73 | * Create Streams 74 | * Get Stream (by id) 75 | * List all streams 76 | * Get metadata for a stream 77 | * Write a single datapoint to a stream 78 | * Write many datapoints to a stream (homogeneous bulk write) 79 | * Write many datapoints to multiple streams (heterogeneous bulk write) 80 | * Read data points from a stream (includes control over order of 81 | returned data set as well as allowing for retrieving data 82 | roll-ups, etc.) 83 | * Support for accessing Device Cloud FileData store 84 | * Get filedata matching a provided condition (path, file extension, 85 | size, etc.) 86 | * Write files to filedata store 87 | * Recursively walk filedata directory tree from some root location 88 | * Get full metadata and contents of files and directories. 89 | * Low level support for performing basic SCI commands with limited parsing 90 | of results and support for only a subset of available services/commands. 91 | * APIs to make direct web service calls to Device Cloud with some details 92 | handled by the library (see DeviceCloudConnection and 'ws' documentation) 93 | * Device Provisioning via Mac Address, IMEI or Device ID 94 | * Monitors 95 | * Creating a TCP or HTTP monitor 96 | 97 | The following features are *not* supported at this time. Feedback on 98 | which features should be highest priority is always welcome. 99 | 100 | * Alarms 101 | * Scheduled Operations 102 | * Asynchronous SCI requests 103 | * High level access to many SCI/RCI operations 104 | * DeviceMetaData 105 | * DeviceVendor 106 | * FileDataHistory 107 | * NetworkInterface support 108 | * XBee specific support (XBeeCore) 109 | * Smart Energy APIs 110 | * SMS Support 111 | * SM/UDP Support 112 | * Carrier Information Access 113 | 114 | Contributing 115 | ------------ 116 | 117 | Contributions to the library are very welcome in whatever form can be provided. This could include issue reports, bug fixes, or features additions. For issue reports, please [create an issue against the Github project](https://github.com/digidotcom/python-devicecloud/issues). 118 | 119 | For code changes, feel free to fork the project on Github and submit a pull request with your changes. Additional instructions for developers contributing to the project can be found in the [Developer's Guide](https://github.com/digidotcom/python-devicecloud/blob/master/CONTRIBUTING.md). 120 | 121 | License 122 | ------- 123 | 124 | This software is open-source software. 125 | 126 | Copyright (c) 2015-2018 Digi International Inc. 127 | 128 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, you can obtain one at https://mozilla.org/MPL/2.0/. 129 | 130 | Digi, Digi International, the Digi logo, the Digi website, Digi Device Cloud, Digi Remote Manager, and Digi Cloud Connector are trademarks or registered trademarks of Digi International Inc. in the United States and other countries worldwide. All other trademarks are the property of their respective owners. 131 | 132 | THE SOFTWARE AND RELATED TECHNICAL INFORMATION IS PROVIDED "AS IS" 133 | WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT 134 | LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 135 | PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL DIGI OR ITS 136 | SUBSIDIARIES BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 137 | WHETHER IN AN ACTION IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT 138 | OF OR IN CONNECTION WITH THE SOFTWARE AND TECHNICAL INFORMATION 139 | HEREIN, INCLUDING ALL SOURCE AND OBJECT CODES, IRRESPECTIVE OF HOW IT 140 | IS USED. YOU AGREE THAT YOU ARE NOT PROHIBITED FROM RECEIVING THIS 141 | SOFTWARE AND TECHNICAL INFORMATION UNDER UNITED STATES AND OTHER 142 | APPLICABLE COUNTRY EXPORT CONTROL LAWS AND REGULATIONS AND THAT YOU 143 | WILL COMPLY WITH ALL APPLICABLE UNITED STATES AND OTHER COUNTRY EXPORT 144 | LAWS AND REGULATIONS WITH REGARD TO USE AND EXPORT OR RE-EXPORT OF THE 145 | SOFTWARE AND TECHNICAL INFORMATION. 146 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | -r test-requirements.txt 2 | coverage 3 | tox 4 | pyandoc 5 | sphinx 6 | sphinx_rtd_theme 7 | -------------------------------------------------------------------------------- /devicecloud/apibase.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | # 5 | # Copyright (c) 2015-2018 Digi International Inc. 6 | 7 | class APIBase(object): 8 | """Base class for all API Classes 9 | 10 | :type _conn: devicecloud.DeviceCloudConnection 11 | """ 12 | def __init__(self, conn): 13 | self._conn = conn 14 | 15 | 16 | class SCIAPIBase(object): 17 | """Base class for API classes using SCI to communicate 18 | 19 | :type _sci_api: devicecloud.sci.ServerCommandInterfaceAPI 20 | """ 21 | def __init__(self, sci_api): 22 | self._sci_api = sci_api 23 | -------------------------------------------------------------------------------- /devicecloud/conditions.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | # 5 | # Copyright (c) 2015-2018 Digi International Inc. 6 | 7 | """Module with functionality for building queries against cloud resources 8 | 9 | This functionality is somewhat poorly documented in Device Cloud documentation 10 | in the `Compound Queries `_ 11 | section. 12 | 13 | """ 14 | import datetime 15 | 16 | from devicecloud.util import isoformat, to_none_or_dt 17 | 18 | 19 | def _quoted(value): 20 | """Return a single-quoted and escaped (percent-encoded) version of value 21 | 22 | This function will also perform transforms of known data types to a representation 23 | that will be handled by Device Cloud. For instance, datetime objects will be 24 | converted to ISO8601. 25 | 26 | """ 27 | if isinstance(value, datetime.datetime): 28 | value = isoformat(to_none_or_dt(value)) 29 | else: 30 | value = str(value) 31 | 32 | return "'{}'".format(value) 33 | 34 | 35 | class Expression(object): 36 | r"""A condition is an evaluable filter 37 | 38 | Examples of conditions would include the following: 39 | * fdType='file' 40 | * fdName like 'sample%gas' 41 | 42 | Conditions may also be compound. E.g. 43 | * (fdType='file' and fdName like 'sample%gas') 44 | 45 | """ 46 | 47 | def __init__(self): 48 | pass 49 | 50 | def __and__(self, rhs): 51 | return Combination(self, " and ", rhs) 52 | 53 | def __or__(self, rhs): 54 | return Combination(self, " or ", rhs) 55 | 56 | and_ = __and__ # alternate syntax 57 | or_ = __or__ # alternate syntax 58 | 59 | def compile(self): 60 | raise NotImplementedError("Should be implemented in subclass") 61 | 62 | 63 | class Combination(Expression): 64 | """A combination combines two expressions""" 65 | 66 | def __init__(self, lhs, sep, rhs): 67 | Expression.__init__(self) 68 | self.lhs = lhs 69 | self.sep = sep 70 | self.rhs = rhs 71 | 72 | def __str__(self): 73 | return self.compile() 74 | 75 | def compile(self): 76 | """Compile this expression into a query string""" 77 | return "{lhs}{sep}{rhs}".format( 78 | lhs=self.lhs.compile(), 79 | sep=self.sep, 80 | rhs=self.rhs.compile(), 81 | ) 82 | 83 | 84 | class Comparison(Expression): 85 | """A comparison is an expression comparing an attribute with a value using some operator""" 86 | 87 | def __init__(self, attribute, sep, value): 88 | Expression.__init__(self) 89 | self.attribute = attribute 90 | self.sep = sep 91 | self.value = value 92 | 93 | def __str__(self): 94 | return self.compile() 95 | 96 | def compile(self): 97 | """Compile this expression into a query string""" 98 | return "{attribute}{sep}{value}".format( 99 | attribute=self.attribute, 100 | sep=self.sep, 101 | value=_quoted(self.value) 102 | ) 103 | 104 | 105 | class Attribute(object): 106 | """An attribute is a piece of data on which we may perform comparisons 107 | 108 | Comparisons performed to attributes will in turn generate new 109 | :class:`.Comparison` instances. 110 | """ 111 | 112 | def __init__(self, name): 113 | self.name = name 114 | 115 | def __str__(self): 116 | return self.name 117 | 118 | def __gt__(self, value): 119 | return Comparison(self, '>', value) 120 | 121 | def __lt__(self, value): 122 | return Comparison(self, '<', value) 123 | 124 | def __eq__(self, value): 125 | return Comparison(self, '=', value) 126 | 127 | def like(self, value): 128 | return Comparison(self, ' like ', value) 129 | -------------------------------------------------------------------------------- /devicecloud/data/__init__.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | # 5 | # Copyright (c) 2015-2018 Digi International Inc. All rights reserved. 6 | # 7 | -------------------------------------------------------------------------------- /devicecloud/data/devicecloud.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIHXTCCBkWgAwIBAgIJANZr7DJFVmHfMA0GCSqGSIb3DQEBCwUAMIG0MQswCQYD 3 | VQQGEwJVUzEQMA4GA1UECBMHQXJpem9uYTETMBEGA1UEBxMKU2NvdHRzZGFsZTEa 4 | MBgGA1UEChMRR29EYWRkeS5jb20sIEluYy4xLTArBgNVBAsTJGh0dHA6Ly9jZXJ0 5 | cy5nb2RhZGR5LmNvbS9yZXBvc2l0b3J5LzEzMDEGA1UEAxMqR28gRGFkZHkgU2Vj 6 | dXJlIENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTE3MDIxMzIxMzIwMVoX 7 | DTE5MDMxMjEzMTgzOFowgdcxEzARBgsrBgEEAYI3PAIBAxMCVVMxGjAYBgsrBgEE 8 | AYI3PAIBAhMJTWlubmVzb3RhMR0wGwYDVQQPExRQcml2YXRlIE9yZ2FuaXphdGlv 9 | bjEOMAwGA1UEBRMFNDg4NTkxCzAJBgNVBAYTAlVTMRIwEAYDVQQIEwlNaW5uZXNv 10 | dGExEzARBgNVBAcTCk1pbm5ldG9ua2ExIDAeBgNVBAoTF0RpZ2kgSW50ZXJuYXRp 11 | b25hbCBJbmMuMR0wGwYDVQQDExRkZXZpY2VjbG91ZC5kaWdpLmNvbTCCASIwDQYJ 12 | KoZIhvcNAQEBBQADggEPADCCAQoCggEBAOiQG1upAjqcisuXt+Kz/3RTbbWli8q1 13 | a4KtcIg0y19gClNsSWVioHH4KFBlYEafcL42MT3LP5+WMEGedBzf6rm7b7MW4IvY 14 | zMUqilUWxXhMtgFRZP/NQlugFCGVL529qult/ZZ6Oo+DTdBukduEdCtXU1KJuFwh 15 | MP1zL85TxTyAt2t7TrWO5Pat9lQGxd/LjC0mI7TdQ++9VNsAzDcYWrP9TSGM8wsr 16 | lU739jLQhRz6Q4UYUPFk8PEvp6x4wdxFMhTqOIQ4McOkjokHLd6WXcjMhY4tPgTJ 17 | hNuiYgtoGj+EGI2XDWPDAc+dQTKWfMa+ykZxbyb4lXyqldARir/OrwsCAwEAAaOC 18 | A0swggNHMAwGA1UdEwEB/wQCMAAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUF 19 | BwMCMA4GA1UdDwEB/wQEAwIFoDA1BgNVHR8ELjAsMCqgKKAmhiRodHRwOi8vY3Js 20 | LmdvZGFkZHkuY29tL2dkaWcyczMtNy5jcmwwXAYDVR0gBFUwUzBIBgtghkgBhv1t 21 | AQcXAzA5MDcGCCsGAQUFBwIBFitodHRwOi8vY2VydGlmaWNhdGVzLmdvZGFkZHku 22 | Y29tL3JlcG9zaXRvcnkvMAcGBWeBDAEBMHYGCCsGAQUFBwEBBGowaDAkBggrBgEF 23 | BQcwAYYYaHR0cDovL29jc3AuZ29kYWRkeS5jb20vMEAGCCsGAQUFBzAChjRodHRw 24 | Oi8vY2VydGlmaWNhdGVzLmdvZGFkZHkuY29tL3JlcG9zaXRvcnkvZ2RpZzIuY3J0 25 | MB8GA1UdIwQYMBaAFEDCvSeOzDSDMKIz1/tss/C0LIDOMDkGA1UdEQQyMDCCFGRl 26 | dmljZWNsb3VkLmRpZ2kuY29tghh3d3cuZGV2aWNlY2xvdWQuZGlnaS5jb20wHQYD 27 | VR0OBBYEFFvYGl4ynfmb11I5vDrDq1/excTEMIIBfgYKKwYBBAHWeQIEAgSCAW4E 28 | ggFqAWgAdgBWFAaaL9fC7NP14b1Esj7HRna5vJkRXMDvlJhV1onQ3QAAAVo5ZQR8 29 | AAAEAwBHMEUCIEQfzk/nT9zPlxJEnYsI0jdcPqIhFTYCevqyAE4EXrgYAiEAmeWb 30 | BVXzM52QxKPOtXpgKOMPjUihpqqgJ9mKjmejgI4AdQDuS723dc5guuFCaR+r4Z5m 31 | ow9+X7By2IMAxHuJeqj9ywAAAVo5ZQhLAAAEAwBGMEQCIAjkl2UpriTtD4bdTjTD 32 | 4IUMaYZMN0NdVsDk5oxSJxCiAiAOtR7KSAKkRn0gEdyTrJpQPolgCLNHj55GPOwZ 33 | oDzX3AB3AKS5CZC0GFgUh7sTosxncAo8NZgE+RvfuON3zQ7IDdwQAAABWjllCWQA 34 | AAQDAEgwRgIhAMw0R6P4MFmCGO1x+QgHeBBWYeVQvMXhGbwN+Ffh4OXoAiEA/kOC 35 | UfexoAx+PmKQ1cQKTKkRCzLhTxzCAOaySMrm/iAwDQYJKoZIhvcNAQELBQADggEB 36 | AGLrc578DVbChRu29j/+c4Q9jlLW8WARTcAbK3dlAX28Hx3jlLEkfWA2aFCoQsva 37 | thgGik6sMOUTkAzV8Qof+6akyeNaVh2yIByNupsnXgsC1SM5qD4UmfBdPGwzW3Xd 38 | RWGN+LM6cmGBzxNOVH2NxdZ/XCGm282tmBnDsXioIQ7fo0EW681svJRCUwbYv4mm 39 | 8wwX8ALNVlODfhs4TwxASOMB2+uFEXRzJ15BawGeXDPg7C1o/giHru6vbO2DVCga 40 | 7eeCxCnM4NJ/dCzc2+K6FqNGL3ddQzAmEtwh4frHEs2KK6/fP2FeZA2WzOTJ4ZXx 41 | jcicTeajht5zfh8/I94rwJI= 42 | -----END CERTIFICATE----- 43 | -----BEGIN CERTIFICATE----- 44 | MIIE0DCCA7igAwIBAgIBBzANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx 45 | EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT 46 | EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp 47 | ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTExMDUwMzA3MDAwMFoXDTMxMDUwMzA3 48 | MDAwMFowgbQxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH 49 | EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjEtMCsGA1UE 50 | CxMkaHR0cDovL2NlcnRzLmdvZGFkZHkuY29tL3JlcG9zaXRvcnkvMTMwMQYDVQQD 51 | EypHbyBEYWRkeSBTZWN1cmUgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IC0gRzIwggEi 52 | MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC54MsQ1K92vdSTYuswZLiBCGzD 53 | BNliF44v/z5lz4/OYuY8UhzaFkVLVat4a2ODYpDOD2lsmcgaFItMzEUz6ojcnqOv 54 | K/6AYZ15V8TPLvQ/MDxdR/yaFrzDN5ZBUY4RS1T4KL7QjL7wMDge87Am+GZHY23e 55 | cSZHjzhHU9FGHbTj3ADqRay9vHHZqm8A29vNMDp5T19MR/gd71vCxJ1gO7GyQ5HY 56 | pDNO6rPWJ0+tJYqlxvTV0KaudAVkV4i1RFXULSo6Pvi4vekyCgKUZMQWOlDxSq7n 57 | eTOvDCAHf+jfBDnCaQJsY1L6d8EbyHSHyLmTGFBUNUtpTrw700kuH9zB0lL7AgMB 58 | AAGjggEaMIIBFjAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNV 59 | HQ4EFgQUQMK9J47MNIMwojPX+2yz8LQsgM4wHwYDVR0jBBgwFoAUOpqFBxBnKLbv 60 | 9r0FQW4gwZTaD94wNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUFBzABhhhodHRwOi8v 61 | b2NzcC5nb2RhZGR5LmNvbS8wNQYDVR0fBC4wLDAqoCigJoYkaHR0cDovL2NybC5n 62 | b2RhZGR5LmNvbS9nZHJvb3QtZzIuY3JsMEYGA1UdIAQ/MD0wOwYEVR0gADAzMDEG 63 | CCsGAQUFBwIBFiVodHRwczovL2NlcnRzLmdvZGFkZHkuY29tL3JlcG9zaXRvcnkv 64 | MA0GCSqGSIb3DQEBCwUAA4IBAQAIfmyTEMg4uJapkEv/oV9PBO9sPpyIBslQj6Zz 65 | 91cxG7685C/b+LrTW+C05+Z5Yg4MotdqY3MxtfWoSKQ7CC2iXZDXtHwlTxFWMMS2 66 | RJ17LJ3lXubvDGGqv+QqG+6EnriDfcFDzkSnE3ANkR/0yBOtg2DZ2HKocyQetawi 67 | DsoXiWJYRBuriSUBAA/NxBti21G00w9RKpv0vHP8ds42pM3Z2Czqrpv1KrKQ0U11 68 | GIo/ikGQI31bS/6kA1ibRrLDYGCD+H1QQc7CoZDDu+8CL9IVVO5EFdkKrqeKM+2x 69 | LXY2JtwE65/3YR8V3Idv7kaWKK2hJn0KCacuBKONvPi8BDAB 70 | -----END CERTIFICATE----- 71 | -----BEGIN CERTIFICATE----- 72 | MIIEfTCCA2WgAwIBAgIDG+cVMA0GCSqGSIb3DQEBCwUAMGMxCzAJBgNVBAYTAlVT 73 | MSEwHwYDVQQKExhUaGUgR28gRGFkZHkgR3JvdXAsIEluYy4xMTAvBgNVBAsTKEdv 74 | IERhZGR5IENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTQwMTAx 75 | MDcwMDAwWhcNMzEwNTMwMDcwMDAwWjCBgzELMAkGA1UEBhMCVVMxEDAOBgNVBAgT 76 | B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoTEUdvRGFkZHku 77 | Y29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRpZmljYXRlIEF1 78 | dGhvcml0eSAtIEcyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv3Fi 79 | CPH6WTT3G8kYo/eASVjpIoMTpsUgQwE7hPHmhUmfJ+r2hBtOoLTbcJjHMgGxBT4H 80 | Tu70+k8vWTAi56sZVmvigAf88xZ1gDlRe+X5NbZ0TqmNghPktj+pA4P6or6KFWp/ 81 | 3gvDthkUBcrqw6gElDtGfDIN8wBmIsiNaW02jBEYt9OyHGC0OPoCjM7T3UYH3go+ 82 | 6118yHz7sCtTpJJiaVElBWEaRIGMLKlDliPfrDqBmg4pxRyp6V0etp6eMAo5zvGI 83 | gPtLXcwy7IViQyU0AlYnAZG0O3AqP26x6JyIAX2f1PnbU21gnb8s51iruF9G/M7E 84 | GwM8CetJMVxpRrPgRwIDAQABo4IBFzCCARMwDwYDVR0TAQH/BAUwAwEB/zAOBgNV 85 | HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFDqahQcQZyi27/a9BUFuIMGU2g/eMB8GA1Ud 86 | IwQYMBaAFNLEsNKR1EwRcbNhyz2h/t2oatTjMDQGCCsGAQUFBwEBBCgwJjAkBggr 87 | BgEFBQcwAYYYaHR0cDovL29jc3AuZ29kYWRkeS5jb20vMDIGA1UdHwQrMCkwJ6Al 88 | oCOGIWh0dHA6Ly9jcmwuZ29kYWRkeS5jb20vZ2Ryb290LmNybDBGBgNVHSAEPzA9 89 | MDsGBFUdIAAwMzAxBggrBgEFBQcCARYlaHR0cHM6Ly9jZXJ0cy5nb2RhZGR5LmNv 90 | bS9yZXBvc2l0b3J5LzANBgkqhkiG9w0BAQsFAAOCAQEAWQtTvZKGEacke+1bMc8d 91 | H2xwxbhuvk679r6XUOEwf7ooXGKUwuN+M/f7QnaF25UcjCJYdQkMiGVnOQoWCcWg 92 | OJekxSOTP7QYpgEGRJHjp2kntFolfzq3Ms3dhP8qOCkzpN1nsoX+oYggHFCJyNwq 93 | 9kIDN0zmiN/VryTyscPfzLXs4Jlet0lUIDyUGAzHHFIYSaRt4bNYC8nY7NmuHDKO 94 | KHAN4v6mF56ED71XcLNa6R+ghlO773z/aQvgSMO3kwvIClTErF0UZzdsyqUvMQg3 95 | qm5vjLyb4lddJIGvl5echK1srDdMZvNhkREg5L4wn3qkKQmw4TRfZHcYQFHfjDCm 96 | rw== 97 | -----END CERTIFICATE----- 98 | -----BEGIN CERTIFICATE----- 99 | MIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEh 100 | MB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBE 101 | YWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3 102 | MDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkGA1UEBhMCVVMxITAfBgNVBAoTGFRo 103 | ZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28gRGFkZHkgQ2xhc3Mg 104 | MiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQADggEN 105 | ADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCA 106 | PVYYYwhv2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6w 107 | wdhFJ2+qN1j3hybX2C32qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXi 108 | EqITLdiOr18SPaAIBQi2XKVlOARFmR6jYGB0xUGlcmIbYsUfb18aQr4CUWWoriMY 109 | avx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmYvLEHZ6IVDd2gWMZEewo+ 110 | YihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0OBBYEFNLE 111 | sNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h 112 | /t2oatTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5 113 | IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmlj 114 | YXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD 115 | ggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wimPQoZ+YeAEW5p5JYXMP80kWNy 116 | OO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKtI3lpjbi2Tc7P 117 | TMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ 118 | HmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mER 119 | dEr/VxqHD3VILs9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5Cuf 120 | ReYNnyicsbkqWletNw+vHX/bvZ8= 121 | -----END CERTIFICATE----- 122 | -------------------------------------------------------------------------------- /devicecloud/examples/__init__.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | # 5 | # Copyright (c) 2015-2018 Digi International Inc. 6 | -------------------------------------------------------------------------------- /devicecloud/examples/cookbook_streams.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import pprint 3 | import random 4 | import time 5 | import json 6 | 7 | from devicecloud.examples.example_helpers import get_authenticated_dc 8 | 9 | from devicecloud.streams import STREAM_TYPE_STRING, DataPoint, STREAM_TYPE_INTEGER 10 | 11 | 12 | def get_or_create_classroom(datatype): 13 | dc = get_authenticated_dc() 14 | classroom = dc.streams.get_stream_if_exists('classroom') 15 | if not classroom: 16 | classroom = dc.streams.create_stream( 17 | stream_id='classroom', 18 | data_type=datatype, 19 | description='Stream representing a classroom of students', 20 | ) 21 | return classroom 22 | 23 | 24 | def fill_classroom_with_student_ids(classroom): 25 | # fake data with wide range of timestamps 26 | now = time.time() 27 | one_day_in_seconds = 86400 28 | 29 | datapoints = list() 30 | for student_id in xrange(100): 31 | deviation = random.randint(0, one_day_in_seconds) 32 | random_time = now + deviation 33 | datapoint = DataPoint(data=student_id, 34 | timestamp=datetime.datetime.fromtimestamp(random_time), 35 | data_type=STREAM_TYPE_INTEGER) 36 | datapoints.append(datapoint) 37 | 38 | classroom.bulk_write_datapoints(datapoints) 39 | 40 | 41 | def example_1(): 42 | classroom = get_or_create_classroom(STREAM_TYPE_STRING) 43 | 44 | student = { 45 | 'name': 'Bob', 46 | 'student_id': 12, 47 | 'age': 21, 48 | } 49 | datapoint = DataPoint(data=json.dumps(student)) 50 | classroom.write(datapoint) 51 | 52 | students = [ 53 | { 54 | 'name': 'James', 55 | 'student_id': 13, 56 | 'age': 22, 57 | }, 58 | { 59 | 'name': 'Henry', 60 | 'student_id': 14, 61 | 'age': 20, 62 | } 63 | ] 64 | datapoints = [DataPoint(data=json.dumps(x)) for x in students] 65 | classroom.bulk_write_datapoints(datapoints) 66 | 67 | most_recent_dp = classroom.get_current_value() 68 | print(json.loads(most_recent_dp.get_data())['name']) 69 | 70 | 71 | def example_2(): 72 | # assume `fill_classroom_with_random_data()` has already been called 73 | 74 | classroom = get_or_create_classroom(STREAM_TYPE_INTEGER) 75 | rollup_data = classroom.read(rollup_interval='hour', rollup_method='count') 76 | hourly_data = {} 77 | for dp in rollup_data: 78 | hourly_data[dp.get_timestamp().hour] = dp.get_data() 79 | pprint.pprint(hourly_data) 80 | 81 | 82 | example_2() 83 | print('done.') 84 | -------------------------------------------------------------------------------- /devicecloud/examples/devicecore_playground.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | # 5 | # Copyright (c) 2015-2018 Digi International Inc. 6 | from devicecloud.devicecore import dev_mac, group_path 7 | from devicecloud.examples.example_helpers import get_authenticated_dc 8 | 9 | 10 | def show_group_tree(dc): 11 | stats = {} # group -> devices count including children 12 | 13 | def count_nodes(group): 14 | count_for_this_node = \ 15 | len(list(dc.devicecore.get_devices(group_path == group.get_path()))) 16 | subnode_count = 0 17 | for child in group.get_children(): 18 | subnode_count += count_nodes(child) 19 | total = count_for_this_node + subnode_count 20 | stats[group] = total 21 | return total 22 | 23 | count_nodes(dc.devicecore.get_group_tree_root()) 24 | print(stats) 25 | dc.devicecore.get_group_tree_root().print_subtree() 26 | 27 | 28 | if __name__ == '__main__': 29 | dc = get_authenticated_dc() 30 | devices = dc.devicecore.get_devices( 31 | (dev_mac == '00:40:9D:50:B0:EA') 32 | ) 33 | for dev in devices: 34 | print(dev) 35 | 36 | show_group_tree(dc) 37 | -------------------------------------------------------------------------------- /devicecloud/examples/example_helpers.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | # 5 | # Copyright (c) 2015-2018 Digi International Inc. 6 | from getpass import getpass 7 | import os 8 | from six.moves import input 9 | from devicecloud import DeviceCloud 10 | 11 | 12 | def get_authenticated_dc(): 13 | while True: 14 | base_url = os.environ.get('DC_BASE_URL', 'https://devicecloud.digi.com') 15 | 16 | username = os.environ.get('DC_USERNAME', None) 17 | if not username: 18 | username = input("username: ") 19 | 20 | password = os.environ.get('DC_PASSWORD', None) 21 | if not password: 22 | password = getpass("password: ") 23 | 24 | dc = DeviceCloud(username, password, base_url=base_url) 25 | if dc.has_valid_credentials(): 26 | print("Credentials accepted!") 27 | return dc 28 | else: 29 | print("Invalid username or password provided, try again") 30 | -------------------------------------------------------------------------------- /devicecloud/examples/file_system_service_playground.py: -------------------------------------------------------------------------------- 1 | import time 2 | import six 3 | from devicecloud.examples.example_helpers import get_authenticated_dc 4 | from devicecloud.file_system_service import ErrorInfo, FileSystemServiceCommandBlock, LsCommand, PutCommand 5 | from devicecloud.sci import DeviceTarget 6 | 7 | 8 | def put_test_file(fssapi, target, tmp_file_path): 9 | tmp_str = six.b('testing string') 10 | 11 | print("\nWriting test file {}".format(tmp_file_path)) 12 | print(fssapi.put_file(target, tmp_file_path, file_data=tmp_str)) 13 | 14 | 15 | def list_contents(fssapi, target, dev_dir): 16 | print("\nList of files in {}".format(dev_dir)) 17 | out_dict = fssapi.list_files(target, dev_dir) 18 | print("list_files returned: {}".format(str(out_dict))) 19 | print_list_contents(out_dict) 20 | 21 | 22 | def print_list_contents(out_dict): 23 | print("\nPrint each item from list_files:") 24 | for device_id, device_data in six.iteritems(out_dict): 25 | print("Items from device {}".format(device_id)) 26 | if isinstance(device_data, ErrorInfo): 27 | print(" ErrorInfo: {}".format(device_data)) 28 | else: 29 | (dirs, files) = device_data 30 | if len(dirs) + len(files) == 0: 31 | print " None" 32 | for d in dirs: 33 | print(" Directory: {}".format(str(d))) 34 | for f in files: 35 | print(" File: {}".format(str(f))) 36 | 37 | 38 | def delete_test_file(fssapi, target, tmp_file_path): 39 | print("\nDeleting test file: {}".format(tmp_file_path)) 40 | print(fssapi.delete_file(target, tmp_file_path)) 41 | 42 | 43 | def get_modified_files(fssapi, target, dev_dir, last_modified_cutoff): 44 | print("\nGetting all files modified since {}".format(last_modified_cutoff)) 45 | out_dict = fssapi.get_modified_items(target, dev_dir, last_modified_cutoff) 46 | print_list_contents(out_dict) 47 | 48 | 49 | def use_filesystem(dc, target, base_dir): 50 | fssapi = dc.get_fss_api() 51 | fd = dc.get_filedata_api() 52 | 53 | tmp_file = '{}/test.txt'.format(base_dir) 54 | 55 | put_test_file(fssapi, target, tmp_file) 56 | 57 | tmp_server_file = 'test_file.txt' 58 | print("\nWriting temp file to server {}".format(tmp_server_file)) 59 | fd.write_file("/~/test_dir/", "test_file.txt", six.b("Hello, world!"), "text/plain") 60 | 61 | tmp_server_device_file = '{}/{}'.format(base_dir, tmp_server_file) 62 | print("\nWriting temp file from server {}".format(tmp_server_device_file)) 63 | fssapi.put_file(target, tmp_server_device_file, server_file='/~/test_dir/{}'.format(tmp_server_file)) 64 | 65 | list_contents(fssapi, target, base_dir) 66 | 67 | print("\nUsing API to get file data") 68 | print(fssapi.get_file(target, tmp_file)) 69 | 70 | print("\nUsing API to get other file data") 71 | print(fssapi.get_file(target, tmp_server_device_file)) 72 | 73 | print("\nUsing API to get partial file data") 74 | print(fssapi.get_file(target, tmp_file, offset=3, length=4)) 75 | 76 | print("\nUsing API to write part of a file") 77 | fssapi.put_file(target, tmp_file, file_data=six.b("what"), offset=4) 78 | print(fssapi.get_file(target, tmp_file)) 79 | 80 | print("\nUsing API to write part of a file and truncating") 81 | fssapi.put_file(target, tmp_file, file_data=six.b("why"), offset=4, truncate=True) 82 | print(fssapi.get_file(target, tmp_file)) 83 | 84 | delete_test_file(fssapi, target, tmp_file) 85 | delete_test_file(fssapi, target, tmp_server_device_file) 86 | 87 | print("\nList of files in {}".format(base_dir)) 88 | out_dict = fssapi.list_files(target, base_dir) 89 | print(out_dict) 90 | 91 | 92 | if __name__ == "__main__": 93 | dc = get_authenticated_dc() 94 | device_id = "your-device-id-here" 95 | target = DeviceTarget(device_id) 96 | base_dir = '/a/directory/on/your/device' 97 | use_filesystem(dc, target, base_dir) 98 | 99 | fssapi = dc.get_fss_api() 100 | tmp_file_path = "{}/{}".format(base_dir, 'test_file.txt') 101 | put_test_file(fssapi, target, tmp_file_path) 102 | cutoff_time = time.time() 103 | get_modified_files(fssapi, target, base_dir, cutoff_time) 104 | print("\nModifying file {}".format(tmp_file_path)) 105 | fssapi.put_file(target, tmp_file_path, file_data=six.b("data"), offset=4) 106 | time.sleep(5) 107 | get_modified_files(fssapi, target, base_dir, cutoff_time) 108 | delete_test_file(fssapi, target, tmp_file_path) 109 | 110 | command_block = FileSystemServiceCommandBlock() 111 | command_block.add_command(LsCommand(base_dir)) 112 | command_block.add_command(LsCommand('/another/directory/on/your/device')) 113 | 114 | print(fssapi.send_command_block(target, command_block)) 115 | -------------------------------------------------------------------------------- /devicecloud/examples/filedata_playground.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | # 5 | # Copyright (c) 2015-2018 Digi International Inc. 6 | 7 | from devicecloud.examples.example_helpers import get_authenticated_dc 8 | from devicecloud.filedata import fd_path 9 | import six 10 | 11 | if __name__ == '__main__': 12 | dc = get_authenticated_dc() 13 | 14 | dc.filedata.write_file("/~/test_dir/", "test_file.txt", six.b("Helllo, world!"), "text/plain") 15 | dc.filedata.write_file("/~/test_dir/", "test_file2.txt", six.b("Hello, again!")) 16 | 17 | for dirpath, directories, files in dc.filedata.walk("/"): 18 | for fd_file in files: 19 | print(fd_file) 20 | 21 | for fd in dc.filedata.get_filedata(fd_path == "~/"): 22 | print (fd) 23 | -------------------------------------------------------------------------------- /devicecloud/examples/monitor_playground.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | # 5 | # Copyright (C) 2015, Digi International, Inc.. 6 | import pprint 7 | import random 8 | import time 9 | 10 | from devicecloud.examples.example_helpers import get_authenticated_dc 11 | 12 | from devicecloud.streams import DataPoint 13 | 14 | def test_tcp_monitor(dc): 15 | # Create a fresh monitor over a pretty broad set of topics 16 | topics = ['DeviceCore', 'FileDataCore', 'FileData', 'DataPoint'] 17 | mon = dc.monitor.get_monitor(topics) 18 | if mon is not None: 19 | mon.delete() 20 | mon = dc.monitor.create_tcp_monitor(topics) 21 | pprint.pprint(mon.get_metadata()) 22 | 23 | def listener(data): 24 | pprint.pprint(data) 25 | return True # we got it! 26 | 27 | mon.add_callback(listener) 28 | 29 | test_stream = dc.streams.get_stream("test") 30 | try: 31 | while True: 32 | test_stream.write(DataPoint(random.random())) 33 | time.sleep(3.14) 34 | except KeyboardInterrupt: 35 | print("Shutting down threads...") 36 | 37 | dc.monitor.stop_listeners() 38 | 39 | def test_http_monitor(dc): 40 | # Create a fresh monitor over a pretty broad set of topics 41 | topics = ['DeviceCore', 'FileDataCore', 'FileData', 'DataPoint'] 42 | mon = dc.monitor.get_monitor(topics) 43 | if mon is not None: 44 | mon.delete() 45 | mon = dc.monitor.create_http_monitor(topics, 'http://digi.com', transport_token=None, transport_method='PUT', 46 | connect_timeout=0, response_timeout=0, batch_size=1, batch_duration=0, 47 | compression='none', format_type='json') 48 | pprint.pprint(mon.get_metadata()) 49 | 50 | def listener(data): 51 | pprint.pprint(data) 52 | return True # we got it! 53 | 54 | test_stream = dc.streams.get_stream("test") 55 | try: 56 | while True: 57 | test_stream.write(DataPoint(random.random())) 58 | time.sleep(3.14) 59 | except KeyboardInterrupt: 60 | print("Shutting down threads...") 61 | 62 | if __name__ == '__main__': 63 | dc = get_authenticated_dc() 64 | test_http_monitor(dc) 65 | 66 | -------------------------------------------------------------------------------- /devicecloud/examples/streams_playground.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | # 5 | # Copyright (c) 2015-2018 Digi International Inc. 6 | from math import pi 7 | import pprint 8 | import time 9 | 10 | from devicecloud.examples.example_helpers import get_authenticated_dc 11 | 12 | from devicecloud.streams import DataPoint, NoSuchStreamException, STREAM_TYPE_INTEGER, STREAM_TYPE_JSON 13 | 14 | 15 | def create_stream_and_delete(dc): 16 | # get a test stream reference 17 | test_stream = dc.streams.get_stream_if_exists("test") 18 | 19 | # we want a clean stream to work with. If the stream exists, nuke it 20 | if test_stream is not None: 21 | test_stream.delete() 22 | 23 | test_stream = dc.streams.create_stream( 24 | stream_id="test", 25 | data_type='float', 26 | description='a stream used for testing', 27 | units='some-unit', 28 | ) 29 | 30 | for i in range(5): 31 | test_stream.write(DataPoint( 32 | data=i * pi, 33 | description="This is {} * pi".format(i) 34 | )) 35 | 36 | for i, stream in enumerate(test_stream.read()): 37 | print ("{}, {!r}".format(i + 1, stream)) 38 | 39 | raw_input("We wrote some points to the cloud, go check it out!") 40 | 41 | # now cleanup by deleting the stream 42 | test_stream.delete() 43 | 44 | 45 | def attempt_to_delete_non_existant(dc): 46 | try: 47 | dc.streams.get_stream("a/nonexistant/stream").delete() 48 | except NoSuchStreamException: 49 | print ("The stream that doesn't exist could not be deleted " 50 | "because it does not exist!") 51 | else: 52 | print ("!!! We were able to delete something which does not exist!!!") 53 | 54 | 55 | def write_points_and_delete_some(dc): 56 | test_stream = dc.streams.get_stream_if_exists("test") 57 | 58 | if test_stream is not None: 59 | test_stream.delete() 60 | 61 | # get a test stream reference 62 | test_stream = dc.streams.get_stream_if_exists("test") 63 | 64 | # we want a clean stream to work with. If the stream exists, nuke it 65 | if test_stream is not None: 66 | test_stream.delete() 67 | 68 | test_stream = dc.streams.create_stream( 69 | stream_id="test", 70 | data_type='float', 71 | description='a stream used for testing', 72 | units='some-unit', 73 | ) 74 | 75 | print("Writing data points with five second delay") 76 | for i in range(5): 77 | print("Writing point {} / 5".format(i + 1)) 78 | test_stream.write(DataPoint( 79 | data=i * 1000, 80 | description="This is {} * pi".format(i) 81 | )) 82 | if i < (5 - 1): 83 | time.sleep(1) 84 | 85 | points = list(test_stream.read(newest_first=False)) 86 | print("Read {} data points, removing the first".format(len(points))) 87 | 88 | # Remove the first 89 | test_stream.delete_datapoint(points[0]) 90 | points = list(test_stream.read(newest_first=False)) 91 | print("Read {} data points, removing ones written in last 30 seconds".format(len(points))) 92 | 93 | # delete the ones in the middle 94 | test_stream.delete_datapoints_in_time_range( 95 | start_dt=points[1].get_timestamp(), 96 | end_dt=points[-1].get_timestamp() 97 | ) 98 | points = list(test_stream.read(newest_first=False)) 99 | print("Read {} data points. Will try to delete all next".format(len(points))) 100 | pprint.pprint(points) 101 | 102 | # let's try without any range at all and see if they all get deleted 103 | test_stream.delete_datapoints_in_time_range() 104 | points = list(test_stream.read(newest_first=False)) 105 | print("Read {} data points".format(len(points))) 106 | 107 | test_stream.delete() 108 | 109 | 110 | def bulk_write_datapoints_single_stream(dc): 111 | datapoints = [] 112 | for i in range(300): 113 | datapoints.append(DataPoint( 114 | data_type=STREAM_TYPE_INTEGER, 115 | units="meters", 116 | data=i, 117 | )) 118 | 119 | stream = dc.streams.get_stream("my/test/bulkstream") 120 | stream.bulk_write_datapoints(datapoints) 121 | print("---" + stream.get_stream_id() + "---") 122 | print(" ".join(str(dp.get_data()) for dp in stream.read(newest_first=False))) 123 | print("") 124 | stream.delete() 125 | 126 | 127 | def bulk_write_datapoints_multiple_streams(dc): 128 | datapoints = [] 129 | for i in range(300): 130 | datapoints.append(DataPoint( 131 | stream_id="my/stream%d" % (i % 3), 132 | data_type=STREAM_TYPE_INTEGER, 133 | units="meters", 134 | data=i, 135 | )) 136 | dc.streams.bulk_write_datapoints(datapoints) 137 | 138 | for stream in dc.streams.get_streams(): 139 | if stream.get_stream_id().startswith('my/stream'): 140 | print("---" + stream.get_stream_id() + "---") 141 | print(" ".join(str(dp.get_data()) for dp in stream.read(newest_first=False))) 142 | print("") 143 | stream.delete() 144 | 145 | 146 | def create_and_use_json_stream(dc): 147 | # get a test stream reference 148 | test_stream = dc.streams.get_stream_if_exists("test-json") 149 | 150 | # we want a clean stream to work with. If the stream exists, nuke it 151 | if test_stream is not None: 152 | test_stream.delete() 153 | 154 | test_stream = dc.streams.create_stream( 155 | stream_id="test-json", 156 | data_type=STREAM_TYPE_JSON, 157 | description='a stream used for testing json', 158 | units='international json standard unit (IJSU)', 159 | ) 160 | 161 | test_stream.write(DataPoint( 162 | data_type=STREAM_TYPE_JSON, 163 | data = {'key1': 'value1', 164 | 2: 2, 165 | 'key3': [1, 2, 3]}, 166 | description="Some JSON data in IJSUs", 167 | ) 168 | ) 169 | 170 | time.sleep(5) 171 | 172 | print(test_stream.get_current_value()) 173 | 174 | test_stream.delete() 175 | 176 | if __name__ == '__main__': 177 | dc = get_authenticated_dc() 178 | create_and_use_json_stream(dc) 179 | create_stream_and_delete(dc) 180 | attempt_to_delete_non_existant(dc) 181 | write_points_and_delete_some(dc) 182 | bulk_write_datapoints_single_stream(dc) 183 | bulk_write_datapoints_multiple_streams(dc) 184 | 185 | -------------------------------------------------------------------------------- /devicecloud/filedata.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | # 5 | # Copyright (c) 2015-2018 Digi International Inc. 6 | 7 | """Provide access to Device Cloud filedata API""" 8 | 9 | import base64 10 | 11 | from devicecloud.apibase import APIBase 12 | from devicecloud.conditions import Attribute, Expression 13 | from devicecloud.util import iso8601_to_dt, validate_type 14 | import six 15 | 16 | 17 | fd_path = Attribute("fdPath") 18 | fd_name = Attribute("fdName") 19 | fd_type = Attribute("fdType") 20 | fd_customer_id = Attribute("customer_id") 21 | fd_created_date = Attribute("fdCreatedDate") 22 | fd_last_modified_date = Attribute("fdLastModifiedDate") 23 | fd_content_type = Attribute("fdContentType") 24 | fd_size = Attribute("fdSize") 25 | 26 | 27 | class FileDataAPI(APIBase): 28 | """Encapsulate data and logic required to interact with Device Cloud file data store""" 29 | 30 | def get_filedata(self, condition=None, page_size=1000): 31 | """Return a generator over all results matching the provided condition 32 | 33 | :param condition: An :class:`.Expression` which defines the condition 34 | which must be matched on the filedata that will be retrieved from 35 | file data store. If a condition is unspecified, the following condition 36 | will be used ``fd_path == '~/'``. This condition will match all file 37 | data in this accounts "home" directory (a sensible root). 38 | :type condition: :class:`.Expression` or None 39 | :param int page_size: The number of results to fetch in a single page. Regardless 40 | of the size specified, :meth:`.get_filedata` will continue to fetch pages 41 | and yield results until all items have been fetched. 42 | :return: Generator yielding :class:`.FileDataObject` instances matching the 43 | provided conditions. 44 | 45 | """ 46 | 47 | condition = validate_type(condition, type(None), Expression, *six.string_types) 48 | page_size = validate_type(page_size, *six.integer_types) 49 | if condition is None: 50 | condition = (fd_path == "~/") # home directory 51 | 52 | params = {"embed": "true", "condition": condition.compile()} 53 | for fd_json in self._conn.iter_json_pages("/ws/FileData", page_size=page_size, **params): 54 | yield FileDataObject.from_json(self, fd_json) 55 | 56 | def write_file(self, path, name, data, content_type=None, archive=False, 57 | raw=False): 58 | """Write a file to the file data store at the given path 59 | 60 | :param str path: The path (directory) into which the file should be written. 61 | :param str name: The name of the file to be written. 62 | :param data: The binary data that should be written into the file. 63 | :type data: str (Python2) or bytes (Python3) 64 | :param content_type: The content type for the data being written to the file. May 65 | be left unspecified. 66 | :type content_type: str or None 67 | :param bool archive: If true, history will be retained for various revisions of this 68 | file. If this is not required, leave as false. 69 | :param bool raw: If true, skip the FileData XML headers (necessary for binary files) 70 | 71 | """ 72 | path = validate_type(path, *six.string_types) 73 | name = validate_type(name, *six.string_types) 74 | data = validate_type(data, six.binary_type) 75 | content_type = validate_type(content_type, type(None), *six.string_types) 76 | archive_str = "true" if validate_type(archive, bool) else "false" 77 | 78 | if not path.startswith("/"): 79 | path = "/" + path 80 | if not path.endswith("/"): 81 | path += "/" 82 | name = name.lstrip("/") 83 | 84 | sio = six.moves.StringIO() 85 | if not raw: 86 | if six.PY3: 87 | base64_encoded_data = base64.encodebytes(data).decode('utf-8') 88 | else: 89 | base64_encoded_data = base64.encodestring(data) 90 | 91 | sio.write("") 92 | if content_type is not None: 93 | sio.write("{}".format(content_type)) 94 | sio.write("file") 95 | sio.write("{}".format(base64_encoded_data)) 96 | sio.write("{}".format(archive_str)) 97 | sio.write("") 98 | else: 99 | sio.write(data) 100 | 101 | params = { 102 | "type": "file", 103 | "archive": archive_str 104 | } 105 | self._conn.put( 106 | "/ws/FileData{path}{name}".format(path=path, name=name), 107 | sio.getvalue(), 108 | params=params) 109 | 110 | def delete_file(self, path): 111 | """Delete a file or directory from the filedata store 112 | 113 | This method removes a file or directory (recursively) from 114 | the filedata store. 115 | 116 | :param path: The path of the file or directory to remove 117 | from the file data store. 118 | 119 | """ 120 | path = validate_type(path, *six.string_types) 121 | if not path.startswith("/"): 122 | path = "/" + path 123 | 124 | self._conn.delete("/ws/FileData{path}".format(path=path)) 125 | 126 | def walk(self, root="~/"): 127 | """Emulation of os.walk behavior against Device Cloud filedata store 128 | 129 | This method will yield tuples in the form ``(dirpath, FileDataDirectory's, FileData's)`` 130 | recursively in pre-order (depth first from top down). 131 | 132 | :param str root: The root path from which the search should commence. By default, this 133 | is the root directory for this device cloud account (~). 134 | :return: Generator yielding 3-tuples of dirpath, directories, and files 135 | :rtype: 3-tuple in form (dirpath, list of :class:`FileDataDirectory`, list of :class:`FileDataFile`) 136 | 137 | """ 138 | root = validate_type(root, *six.string_types) 139 | 140 | directories = [] 141 | files = [] 142 | 143 | # fd_path is real picky 144 | query_fd_path = root 145 | if not query_fd_path.endswith("/"): 146 | query_fd_path += "/" 147 | 148 | for fd_object in self.get_filedata(fd_path == query_fd_path): 149 | if fd_object.get_type() == "directory": 150 | directories.append(fd_object) 151 | else: 152 | files.append(fd_object) 153 | 154 | # Yield the walk results for this level of the tree 155 | yield (root, directories, files) 156 | 157 | # recurse on each directory and yield results up the chain 158 | for directory in directories: 159 | for dirpath, directories, files in self.walk(directory.get_full_path()): 160 | yield (dirpath, directories, files) 161 | 162 | 163 | class FileDataObject(object): 164 | """Encapsulate state and logic surrounding a "filedata" element""" 165 | 166 | @classmethod 167 | def from_json(cls, fdapi, json_data): 168 | fd_type = json_data["fdType"] 169 | if fd_type == "directory": 170 | return FileDataDirectory.from_json(fdapi, json_data) 171 | else: 172 | return FileDataFile.from_json(fdapi, json_data) 173 | 174 | def __init__(self, fdapi, json_data): 175 | self._fdapi = fdapi 176 | self._json_data = json_data 177 | 178 | def delete(self): 179 | """Delete this file or directory""" 180 | return self._fdapi.delete_file(self.get_full_path()) 181 | 182 | def get_data(self): 183 | """Get the data associated with this filedata object 184 | 185 | :returns: Data associated with this object or None if none exists 186 | :rtype: str (Python2)/bytes (Python3) or None 187 | 188 | """ 189 | # NOTE: we assume that the "embed" option is used 190 | base64_data = self._json_data.get("fdData") 191 | if base64_data is None: 192 | return None 193 | else: 194 | # need to convert to bytes() with python 3 195 | return base64.decodestring(six.b(base64_data)) 196 | 197 | def get_type(self): 198 | """Get the type (file/directory) of this object""" 199 | return self._json_data["fdType"] 200 | 201 | def get_last_modified_date(self): 202 | """Get the last modified datetime of this object""" 203 | return iso8601_to_dt(self._json_data["fdLastModifiedDate"]) 204 | 205 | def get_content_type(self): 206 | """Get the content type of this object (or None)""" 207 | return self._json_data["fdContentType"] 208 | 209 | def get_customer_id(self): 210 | """Get the customer ID associated with this object""" 211 | return self._json_data["cstId"] 212 | 213 | def get_created_date(self): 214 | """Get the datetime this object was created""" 215 | return iso8601_to_dt(self._json_data["fdCreatedDate"]) 216 | 217 | def get_name(self): 218 | """Get the name of this object""" 219 | return self._json_data["id"]["fdName"] 220 | 221 | def get_path(self): 222 | """Get the path of this object""" 223 | return self._json_data["id"]["fdPath"] 224 | 225 | def get_full_path(self): 226 | """Get the full path (path and name) of this object""" 227 | return "{}{}".format(self.get_path(), self.get_name()) 228 | 229 | def get_size(self): 230 | """Get this size of this object (will be 0 for directories)""" 231 | return int(self._json_data["fdSize"]) 232 | 233 | 234 | class FileDataDirectory(FileDataObject): 235 | """Provide access to a directory and its metadata in the filedata store""" 236 | 237 | @classmethod 238 | def from_json(cls, fdapi, json_data): 239 | return cls(fdapi, json_data) 240 | 241 | def __init__(self, fdapi, data): 242 | FileDataObject.__init__(self, fdapi, data) 243 | 244 | def __repr__(self): 245 | return "FileDataDirectory({!r})".format(self._json_data) 246 | 247 | def walk(self): 248 | """Walk the directories and files rooted with this directory 249 | 250 | This method will yield tuples in the form ``(dirpath, FileDataDirectory's, FileData's)`` 251 | recursively in pre-order (depth first from top down). 252 | 253 | :return: Generator yielding 3-tuples of dirpath, directories, and files 254 | :rtype: 3-tuple in form (dirpath, list of :class:`FileDataDirectory`, list of :class:`FileDataFile`) 255 | 256 | """ 257 | return self._fdapi.walk(root=self.get_path()) 258 | 259 | def write_file(self, *args, **kwargs): 260 | """Write a file into this directory 261 | 262 | This method takes the same arguments as :meth:`.FileDataAPI.write_file` 263 | with the exception of the ``path`` argument which is not needed here. 264 | 265 | """ 266 | return self._fdapi.write_file(self.get_path(), *args, **kwargs) 267 | 268 | 269 | class FileDataFile(FileDataObject): 270 | """Provide access to a file and its metadata in the filedata store""" 271 | 272 | @classmethod 273 | def from_json(cls, fdapi, json_data): 274 | return cls(fdapi, json_data) 275 | 276 | def __init__(self, fdapi, json_data): 277 | FileDataObject.__init__(self, fdapi, json_data) 278 | 279 | def __repr__(self): 280 | return "FileDataFile({!r})".format(self._json_data) 281 | -------------------------------------------------------------------------------- /devicecloud/sci.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | # 5 | # Copyright (c) 2015-2018 Digi International Inc. 6 | 7 | """Server Command Interface functionality""" 8 | from devicecloud.apibase import APIBase 9 | from xml.etree import ElementTree as ET 10 | import six 11 | 12 | 13 | SCI_TEMPLATE = """\ 14 | 15 | <{operation}{attribute}{reply}{synchronous}{cache}{sync_timeout}{allow_offline}{wait_for_reconnect}> 16 | 17 | {targets} 18 | 19 | {payload} 20 | 21 | 22 | """.replace(" ", "").replace("\r", "").replace("\n", "") # two spaces is indentation 23 | 24 | 25 | class TargetABC(object): 26 | """Abstract base class for all target types""" 27 | 28 | 29 | class DeviceTarget(TargetABC): 30 | """Target a specific device""" 31 | 32 | def __init__(self, device_id): 33 | self._device_id = device_id 34 | 35 | def to_xml(self): 36 | return ''.format(self._device_id) 37 | 38 | 39 | class AllTarget(TargetABC): 40 | """Target all devices""" 41 | 42 | def __init__(self): 43 | pass 44 | 45 | def to_xml(self): 46 | return '' 47 | 48 | 49 | class TagTarget(TargetABC): 50 | """Target devices having a specific tag""" 51 | 52 | def __init__(self, tag): 53 | self._tag = tag 54 | 55 | def to_xml(self): 56 | return ''.format(self._tag) 57 | 58 | 59 | class GroupTarget(TargetABC): 60 | """Target devices in a specific group""" 61 | 62 | def __init__(self, group): 63 | self._group = group 64 | 65 | def to_xml(self): 66 | return ''.format(self._group) 67 | 68 | 69 | class AsyncRequestProxy(object): 70 | """An object representing an asynychronous SCI request. 71 | 72 | Can be used for polling the status of the corresponding request. 73 | 74 | :ivar job_id: the ID in device cloud of the job 75 | :ivar response: the response to the request if completed 76 | :ivar completed: True if the request has completed, False otherwise; queries on read 77 | """ 78 | def __init__(self, job_id, conn): 79 | self.job_id = job_id 80 | self._conn = conn 81 | self.response = None 82 | 83 | @property 84 | def completed(self): 85 | if self.response is not None: 86 | return True 87 | resp = self._conn.get('/ws/sci/{0}'.format(self.job_id)) 88 | dom = ET.fromstring(resp.content) 89 | status = dom.find('.//status') 90 | if status is not None and status.text == 'complete': 91 | self.response = resp.content 92 | return True 93 | else: 94 | return False 95 | 96 | 97 | class ServerCommandInterfaceAPI(APIBase): 98 | """Encapsulate Server Command Interface API""" 99 | 100 | def get_async_job(self, job_id): 101 | """Query an asynchronous SCI job by ID 102 | 103 | This is useful if the job was not created with send_sci_async(). 104 | 105 | :param int job_id: The job ID to query 106 | :returns: The SCI response from GETting the job information 107 | """ 108 | uri = "/ws/sci/{0}".format(job_id) 109 | # TODO: do parsing here? 110 | return self._conn.get(uri) 111 | 112 | def send_sci_async(self, operation, target, payload, **sci_options): 113 | """Send an asynchronous SCI request, and wraps the job in an object 114 | to manage it 115 | 116 | :param str operation: The operation is one of {send_message, update_firmware, disconnect, query_firmware_targets, 117 | file_system, data_service, and reboot} 118 | :param target: The device(s) to be targeted with this request 119 | :type target: :class:`~.TargetABC` or list of :class:`~.TargetABC` instances 120 | 121 | TODO: document other params 122 | 123 | """ 124 | sci_options['synchronous'] = False 125 | resp = self.send_sci(operation, target, payload, **sci_options) 126 | dom = ET.fromstring(resp.content) 127 | job_element = dom.find('.//jobId') 128 | if job_element is None: 129 | return 130 | job_id = int(job_element.text) 131 | return AsyncRequestProxy(job_id, self._conn) 132 | 133 | def send_sci(self, operation, target, payload, reply=None, synchronous=None, sync_timeout=None, 134 | cache=None, allow_offline=None, wait_for_reconnect=None, attribute=None): 135 | """Send SCI request to 1 or more targets 136 | 137 | :param str operation: The operation is one of {send_message, update_firmware, disconnect, query_firmware_targets, 138 | file_system, data_service, and reboot} 139 | :param target: The device(s) to be targeted with this request 140 | :type target: :class:`~.TargetABC` or list of :class:`~.TargetABC` instances 141 | 142 | TODO: document other params 143 | 144 | """ 145 | if not isinstance(payload, six.string_types) and not isinstance(payload, six.binary_type): 146 | raise TypeError("payload is required to be a string or bytes") 147 | 148 | # validate targets and bulid targets xml section 149 | try: 150 | iter(target) 151 | targets = target 152 | except TypeError: 153 | targets = [target, ] 154 | if not all(isinstance(t, TargetABC) for t in targets): 155 | raise TypeError("Target(s) must each be instances of TargetABC") 156 | targets_xml = "".join(t.to_xml() for t in targets) 157 | 158 | # reply argument 159 | if not isinstance(reply, (type(None), six.string_types)): 160 | raise TypeError("reply must be either None or a string") 161 | if reply is not None: 162 | reply_xml = ' reply="{}"'.format(reply) 163 | else: 164 | reply_xml = '' 165 | 166 | # synchronous argument 167 | if not isinstance(synchronous, (type(None), bool)): 168 | raise TypeError("synchronous expected to be either None or a boolean") 169 | if synchronous is not None: 170 | synchronous_xml = ' synchronous="{}"'.format('true' if synchronous else 'false') 171 | else: 172 | synchronous_xml = '' 173 | 174 | # sync_timeout argument 175 | # TODO: What units is syncTimeout in? seconds? 176 | if sync_timeout is not None and not isinstance(sync_timeout, six.integer_types): 177 | raise TypeError("sync_timeout expected to either be None or a number") 178 | if sync_timeout is not None: 179 | sync_timeout_xml = ' syncTimeout="{}"'.format(sync_timeout) 180 | else: 181 | sync_timeout_xml = '' 182 | 183 | # cache argument 184 | if not isinstance(cache, (type(None), bool)): 185 | raise TypeError("cache expected to either be None or a boolean") 186 | if cache is not None: 187 | cache_xml = ' cache="{}"'.format('true' if cache else 'false') 188 | else: 189 | cache_xml = '' 190 | 191 | # allow_offline argument 192 | if not isinstance(allow_offline, (type(None), bool)): 193 | raise TypeError("allow_offline is expected to be either None or a boolean") 194 | if allow_offline is not None: 195 | allow_offline_xml = ' allowOffline="{}"'.format('true' if allow_offline else 'false') 196 | else: 197 | allow_offline_xml = '' 198 | 199 | # wait_for_reconnect argument 200 | if not isinstance(wait_for_reconnect, (type(None), bool)): 201 | raise TypeError("wait_for_reconnect expected to be either None or a boolean") 202 | if wait_for_reconnect is not None: 203 | wait_for_reconnect_xml = ' waitForReconnect="{}"'.format('true' if wait_for_reconnect else 'false') 204 | else: 205 | wait_for_reconnect_xml = '' 206 | 207 | if attribute is not None: 208 | operation_attribute = ' ' + attribute 209 | else: 210 | operation_attribute = '' 211 | 212 | full_request = SCI_TEMPLATE.format( 213 | operation=operation, 214 | targets=targets_xml, 215 | reply=reply_xml, 216 | synchronous=synchronous_xml, 217 | sync_timeout=sync_timeout_xml, 218 | cache=cache_xml, 219 | allow_offline=allow_offline_xml, 220 | wait_for_reconnect=wait_for_reconnect_xml, 221 | payload=payload, 222 | attribute=operation_attribute 223 | ) 224 | 225 | # TODO: do parsing here? 226 | return self._conn.post("/ws/sci", full_request) 227 | -------------------------------------------------------------------------------- /devicecloud/test/__init__.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | # 5 | # Copyright (c) 2015-2018 Digi International Inc. 6 | -------------------------------------------------------------------------------- /devicecloud/test/integration/__init__.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | # 5 | # Copyright (c) 2015-2018 Digi International Inc. All rights reserved. 6 | -------------------------------------------------------------------------------- /devicecloud/test/integration/inttest_monitor_tcp.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | # 5 | # Copyright (c) 2015-2018 Digi International Inc. 6 | import pprint 7 | import time 8 | from devicecloud.streams import DataPoint 9 | 10 | from devicecloud.test.integration.inttest_utilities import DeviceCloudIntegrationTestCase 11 | import six 12 | 13 | 14 | class StreamsIntegrationTestCase(DeviceCloudIntegrationTestCase): 15 | 16 | def test_event_reception(self): 17 | rx = [] 18 | 19 | def receive_notification(notification): 20 | rx.append(notification) 21 | return True 22 | 23 | topics = ['DataPoint', 'FileData'] 24 | monitor = self._dc.monitor.get_monitor(topics) 25 | if monitor: 26 | monitor.delete() 27 | monitor = self._dc.monitor.create_tcp_monitor(topics) 28 | monitor.add_callback(receive_notification) 29 | 30 | self._dc.filedata.write_file("/~/inttest/monitor_tcp/", "test_file.txt", six.b("Hello, world!"), "text/plain") 31 | self._dc.streams.get_stream("inttest/monitor_tcp").write(DataPoint(10)) 32 | 33 | # Wait for the evenets to come in from the cloud 34 | time.sleep(3) 35 | self._dc.monitor.stop_listeners() 36 | 37 | try: 38 | fd_push_seen = False 39 | dp_push_seen = False 40 | for rec in rx: 41 | msg = rec['Document']['Msg'] 42 | fd = msg.get('FileData', None) 43 | if fd and 'id' in fd: 44 | if (fd['id']['fdName'] == 'test_file.txt' and 45 | fd['id']['fdPath'] == '/db/7603_Digi/inttest/monitor_tcp/'): 46 | fd_push_seen = True 47 | # else: 48 | # print('id not in test_event_reception/fd: {}'.format(rx)) 49 | dp = msg.get('DataPoint') 50 | if dp and 'streamId' in dp: 51 | print('test_event_reception/dp: {}'.format(dp)) 52 | if dp['streamId'] == 'inttest/monitor_tcp': 53 | dp_push_seen = True 54 | # else: 55 | # print('streamId not in test_event_reception/dp: {}'.format(rx)) 56 | self.assertTrue(fd_push_seen) 57 | self.assertTrue(dp_push_seen) 58 | except: 59 | # add some additional debugging information 60 | pprint.pprint(rx) 61 | raise 62 | 63 | if __name__ == '__main__': 64 | import unittest 65 | unittest.main() 66 | -------------------------------------------------------------------------------- /devicecloud/test/integration/inttest_streams.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | # 5 | # Copyright (c) 2015-2018 Digi International Inc. All rights reserved. 6 | 7 | """Integration tests for streams functionality 8 | 9 | These tests test that the streams functionality actually works against the device 10 | cloud itself. 11 | 12 | """ 13 | from math import pi 14 | import datetime 15 | 16 | from devicecloud.streams import DataPoint, STREAM_TYPE_INTEGER 17 | from devicecloud.test.integration.inttest_utilities import DeviceCloudIntegrationTestCase, dc_inttest_main 18 | import six 19 | 20 | 21 | class StreamsIntegrationTestCase(DeviceCloudIntegrationTestCase): 22 | 23 | def test_basic_nonbulk_stream_operations(self): 24 | # 25 | # This test verifiest that we can perform a number of simple operations on 26 | # a data stream (non-bulk). The ops are create, write, read, and delete 27 | # 28 | SID = "pythondc-inttest/test_basic_nonbulk_stream_operations" 29 | 30 | # get a test stream reference 31 | test_stream = self._dc.streams.get_stream_if_exists(SID) 32 | 33 | # we want a clean stream to work with. If the stream exists, nuke it 34 | if test_stream is not None: 35 | test_stream.delete() 36 | 37 | test_stream = self._dc.streams.create_stream( 38 | stream_id=SID, 39 | data_type='float', 40 | description='a stream used for testing', 41 | units='some-unit', 42 | ) 43 | 44 | for i in range(5): 45 | test_stream.write(DataPoint( 46 | data=i * pi, 47 | description="This is {} * pi".format(i) 48 | )) 49 | 50 | for i, dp in enumerate(test_stream.read(newest_first=False)): 51 | self.assertAlmostEqual(dp.get_data(), i * pi) 52 | 53 | # now cleanup by deleting the stream 54 | test_stream.delete() 55 | 56 | def test_bulk_write_datapoints_multiple_streams(self): 57 | # 58 | # This test verifies that we can write in bulk a bunch of datapoints to several 59 | # datastreams and read them back. 60 | # 61 | SID_FMT = "pythondc-inttest/test_bulk_write_datapoints_multiple_streams-{}" 62 | datapoints = [] 63 | dt = datetime.datetime.now() 64 | for i in range(300): 65 | datapoints.append(DataPoint( 66 | stream_id=SID_FMT.format(i % 3), 67 | data_type=STREAM_TYPE_INTEGER, 68 | units="meters", 69 | timestamp=dt - datetime.timedelta(seconds=300 - i), 70 | data=i, 71 | )) 72 | 73 | # remove any existing data before starting out 74 | for i in range(3): 75 | s = self._dc.streams.get_stream_if_exists(SID_FMT.format(i % 3)) 76 | if s: 77 | s.delete() 78 | 79 | self._dc.streams.bulk_write_datapoints(datapoints) 80 | 81 | for i in range(3): 82 | stream = self._dc.streams.get_stream(SID_FMT.format(i)) 83 | for j, dp in enumerate(stream.read(newest_first=False)): 84 | self.assertEqual(dp.get_data(), j * 3 + i) 85 | stream.delete() 86 | 87 | def test_bulk_write_datapoints_single_stream(self): 88 | # 89 | # This test verifies that we can write in bulk a bunch of datapoints to a single 90 | # stream and read them back. 91 | # 92 | datapoints = [] 93 | dt = datetime.datetime.now() 94 | for i in range(300): 95 | datapoints.append(DataPoint( 96 | data_type=STREAM_TYPE_INTEGER, 97 | units="meters", 98 | timestamp=dt - datetime.timedelta(seconds=300 - i), 99 | data=i, 100 | )) 101 | 102 | stream = self._dc.streams.get_stream_if_exists("pythondc-inttest/test_bulk_write_datapoints_single_stream") 103 | if stream: 104 | stream.delete() 105 | 106 | stream = self._dc.streams.get_stream("pythondc-inttest/test_bulk_write_datapoints_single_stream") 107 | stream.bulk_write_datapoints(datapoints) 108 | stream_contents_asc = list(stream.read(newest_first=False)) 109 | self.assertEqual(len(stream_contents_asc), 300) 110 | for i, dp in enumerate(stream_contents_asc): 111 | self.assertEqual(dp.get_units(), "meters") 112 | self.assertEqual(dp.get_data_type(), STREAM_TYPE_INTEGER) 113 | self.assertEqual(dp.get_data(), i) 114 | self.assertEqual(dp.get_stream_id(), "pythondc-inttest/test_bulk_write_datapoints_single_stream") 115 | self.assertEqual(dp.get_location(), None) 116 | self.assertEqual(dp.get_description(), "") 117 | self.assertIsInstance(dp.get_server_timestamp(), datetime.datetime) 118 | self.assertIsInstance(dp.get_id(), *six.string_types) 119 | self.assertEqual(dp.get_quality(), 0) 120 | self.assertIsInstance(dp.get_timestamp(), datetime.datetime) 121 | 122 | # Cleanup by deleting the stream 123 | stream.delete() 124 | 125 | 126 | if __name__ == '__main__': 127 | dc_inttest_main() 128 | -------------------------------------------------------------------------------- /devicecloud/test/integration/inttest_utilities.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | # 5 | # Copyright (c) 2015-2018 Digi International Inc. All rights reserved. 6 | 7 | from getpass import getpass 8 | import unittest 9 | 10 | from devicecloud import DeviceCloud 11 | import os 12 | from six.moves import input 13 | 14 | 15 | class DeviceCloudIntegrationTestCase(unittest.TestCase): 16 | 17 | def setUp(self): 18 | if not os.environ.get("RUN_INTEGRATION_TESTS", False): 19 | self.skipTest("Not performing integration tests") 20 | else: 21 | self._username = os.environ.get("DC_USERNAME", None) 22 | self._password = os.environ.get("DC_PASSWORD", None) 23 | self._base_url = os.environ.get("DC_URL", None) # will use default if unspecified 24 | if not self._username or not self._password: 25 | self.fail("DC_USERNAME and DC_PASSWORD must be set for integration tests to run") 26 | self._dc = DeviceCloud(self._username, self._password, base_url=self._base_url) 27 | 28 | 29 | def dc_inttest_main(): 30 | """Helper method for kicking off integration tests in a module 31 | 32 | This is used in the same way that one might use 'unittest.main()' in a normal 33 | function. 34 | """ 35 | os.environ["RUN_INTEGRATION_TESTS"] = "yes" 36 | if not os.environ.get("DC_USERNAME"): 37 | os.environ["DC_USERNAME"] = input("username: ") 38 | if not os.environ.get("DC_PASSWORD"): 39 | os.environ["DC_PASSWORD"] = getpass("password: ") 40 | unittest.main() 41 | -------------------------------------------------------------------------------- /devicecloud/test/unit/__init__.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | # 5 | # Copyright (c) 2015-2018 Digi International Inc. All rights reserved. 6 | -------------------------------------------------------------------------------- /devicecloud/test/unit/test_conditions.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | # 5 | # Copyright (c) 2015-2018 Digi International Inc. All rights reserved. 6 | 7 | import unittest 8 | import datetime 9 | 10 | from devicecloud.conditions import Attribute 11 | 12 | 13 | class TestConditions(unittest.TestCase): 14 | 15 | def test_gt(self): 16 | a = Attribute("a") 17 | self.assertEqual((a > 21).compile(), "a>'21'") 18 | 19 | def test_lt(self): 20 | a = Attribute("a") 21 | self.assertEqual((a < 25).compile(), "a<'25'") 22 | 23 | def test_eq(self): 24 | a = Attribute("a") 25 | self.assertEqual((a == "a string").compile(), "a='a string'") 26 | 27 | def test_like(self): 28 | a = Attribute("a") 29 | self.assertEqual(a.like(r"%.txt").compile(), "a like '%.txt'") 30 | 31 | def test_and(self): 32 | a = Attribute("a") 33 | b = Attribute("b") 34 | expr = (a > 21) & (b == "Amsterdam") 35 | self.assertEqual(expr.compile(), "a>'21' and b='Amsterdam'") 36 | 37 | def test_or(self): 38 | a = Attribute("a") 39 | b = Attribute("b") 40 | expr = (a.like("%.csv")) | (b < 1024) 41 | self.assertEqual(expr.compile(), "a like '%.csv' or b<'1024'") 42 | 43 | def test_datacmp(self): 44 | a = Attribute("a") 45 | self.assertEqual((a < datetime.datetime(2014, 7, 7)).compile(), 46 | "a<'2014-07-07T00:00:00Z'") 47 | 48 | def test_multi_combination(self): 49 | a = Attribute("a") 50 | self.assertEqual(((a > 1) & (a > 2) & (a > 3)).compile(), 51 | "a>'1' and a>'2' and a>'3'") 52 | 53 | 54 | if __name__ == '__main__': 55 | unittest.main() 56 | -------------------------------------------------------------------------------- /devicecloud/test/unit/test_core.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | # 5 | # Copyright (c) 2015-2018 Digi International Inc. 6 | import unittest 7 | 8 | from devicecloud import DeviceCloudHttpException 9 | from devicecloud.test.unit.test_utilities import HttpTestBase 10 | from mock import patch, call 11 | import six 12 | 13 | 14 | TEST_BASIC_RESPONSE = """\ 15 | { 16 | "resultTotalRows": "2", 17 | "requestedStartRow": "0", 18 | "resultSize": "2", 19 | "requestedSize": "1000", 20 | "remainingSize": "0", 21 | "items": [ 22 | { "id": 1, "name": "bob" }, 23 | { "id": 2, "name": "tim" } 24 | ] 25 | } 26 | """ 27 | 28 | TEST_PAGED_RESPONSE_PAGE1 = """\ 29 | { 30 | "resultTotalRows": "2", 31 | "requestedStartRow": "0", 32 | "resultSize": "1", 33 | "requestedSize": "1", 34 | "remainingSize": "1", 35 | "items": [ 36 | { "id": 1, "name": "bob" } 37 | ] 38 | } 39 | """ 40 | 41 | TEST_PAGED_RESPONSE_PAGE2 = """\ 42 | { 43 | "resultTotalRows": "2", 44 | "requestedStartRow": "1", 45 | "resultSize": "1", 46 | "requestedSize": "1", 47 | "remainingSize": "0", 48 | "items": [ 49 | { "id": 2, "name": "tim" } 50 | ] 51 | } 52 | """ 53 | 54 | TEST_ERROR_RESPONSE = six.b("""\ 55 | \ 56 | Invalid target. Device not found.\ 57 | Invalid SCI request. No valid targets found.\ 58 | """) 59 | 60 | 61 | class TestDeviceCloudConnection(HttpTestBase): 62 | 63 | @patch("time.sleep", return_value=None) 64 | def test_throttle_retries(self, patched_time_sleep): 65 | self.prepare_response("GET", "/test/path", "", status=429) 66 | self.assertRaises(DeviceCloudHttpException, self.dc.get_connection().get, "/test/path", retries=5) 67 | patched_time_sleep.assert_has_calls([ 68 | call(1.5 ** 0), 69 | call(1.5 ** 1), 70 | call(1.5 ** 2), 71 | call(1.5 ** 3), 72 | call(1.5 ** 4), 73 | ]) 74 | 75 | def test_iter_json_with_params(self): 76 | it = self.dc.get_connection().iter_json_pages("/test/path", foo="bar", key="value") 77 | self.prepare_response("GET", "/test/path", TEST_BASIC_RESPONSE) 78 | self.assertEqual(len(list(it)), 2) 79 | self.assertDictEqual(self._get_last_request_params(), { 80 | "start": "0", 81 | "foo": "bar", 82 | "key": "value", 83 | "size": "1000", 84 | }) 85 | 86 | def test_iter_json_pages_paged_noparams(self): 87 | it = self.dc.get_connection().iter_json_pages("/test/path", page_size=1) 88 | self.prepare_response("GET", "/test/path", TEST_PAGED_RESPONSE_PAGE1) 89 | self.assertEqual(six.next(it)["id"], 1) 90 | self.prepare_response("GET", "/test/path", TEST_PAGED_RESPONSE_PAGE2) 91 | self.assertEqual(six.next(it)["id"], 2) 92 | self.assertDictEqual(self._get_last_request_params(), { 93 | "size": "1", 94 | "start": "1" 95 | }) 96 | 97 | def test_http_exception(self): 98 | self.prepare_response("POST", "/test/path", TEST_ERROR_RESPONSE, status=400) 99 | try: 100 | self.dc.get_connection().post("/test/path", "bad data") 101 | except DeviceCloudHttpException as e: 102 | str(e) # ensure this does not stack trace at least 103 | self.assertEqual(e.response.status_code, 400) 104 | self.assertEqual(e.response.content, TEST_ERROR_RESPONSE) 105 | else: 106 | self.fail("DeviceCloudHttpException not raised") 107 | 108 | if __name__ == "__main__": 109 | unittest.main() 110 | -------------------------------------------------------------------------------- /devicecloud/test/unit/test_filedata.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | # 5 | # Copyright (c) 2015-2018 Digi International Inc. All rights reserved. 6 | 7 | import base64 8 | import unittest 9 | from xml.etree import ElementTree 10 | import datetime 11 | 12 | from dateutil.tz import tzutc 13 | from devicecloud.test.unit.test_utilities import HttpTestBase 14 | import six 15 | 16 | 17 | GET_FILEDATA_SIMPLE = """\ 18 | { 19 | "items": [ 20 | { 21 | "fdType": "file", 22 | "id": { "fdName": "test.txt", "fdPath": "/db/blah/" }, 23 | "fdLastModifiedDate": "2014-07-20T18:46:45.123Z", 24 | "fdContentType": "application/binary", 25 | "cstId": "1234", 26 | "fdCreatedDate": "2014-07-20T18:46:45.123Z", 27 | "fdSize": "1234", 28 | "fdData": "QSBtYW4gYSBwbGFuIGEgY2FuYWwgcGFuYW1h" 29 | }, 30 | { 31 | "fdType": "directory", 32 | "id": { "fdName": "", "fdPath": "/db/blah/" }, 33 | "fdLastModifiedDate": "2014-07-20T18:46:45.123Z", 34 | "fdContentType": "application/xml", 35 | "cstId": "1234", 36 | "fdCreatedDate": "2014-07-20T18:46:45.123Z", 37 | "fdSize": "0" 38 | } 39 | ] 40 | } 41 | """ 42 | 43 | # Paging Test 44 | GET_FILEDATA_PAGE1 = """\ 45 | { 46 | "resultTotalRows": "2", 47 | "requestedStartRow": "0", 48 | "resultSize": "1", 49 | "requestedSize": "1", 50 | "remainingSize": "1", 51 | "items": [ 52 | { "id": { "fdPath": "\/db\/CUS0000033_Spectrum_Design_Solutions__Paul_Osborne\/test_dir\/", "fdName": "test_file.txt"}, "cstId": "304", "fdCreatedDate": "2014-07-13T04:37:16.283Z", "fdLastModifiedDate": "2014-08-07T03:57:09.393Z", "fdContentType": "text\/plain", "fdSize": "149", "fdType": "file", "fdData": "PEZpbGVEYXRhPjxmZENvbnRlbnRUeXBlPnRleHQvcGxhaW48L2ZkQ29udGVudFR5cGU+PGZkVHlwZT5maWxlPC9mZFR5cGU+PGZkRGF0YT5TR1ZzYkd4dkxDQjNiM0pzWkNFPQo8L2ZkRGF0YT48ZmRBcmNoaXZlPmZhbHNlPC9mZEFyY2hpdmU+PC9GaWxlRGF0YT4="} 53 | ] 54 | } 55 | """ 56 | 57 | GET_FILEDATA_PAGE2 = """\ 58 | { 59 | "resultTotalRows": "2", 60 | "requestedStartRow": "1", 61 | "resultSize": "1", 62 | "requestedSize": "1", 63 | "remainingSize": "0", 64 | "items": [ 65 | { "id": { "fdPath": "\/db\/CUS0000033_Spectrum_Design_Solutions__Paul_Osborne\/test_dir\/", "fdName": "test_file2.txt"}, "cstId": "304", "fdCreatedDate": "2014-08-07T03:56:22.013Z", "fdLastModifiedDate": "2014-08-07T03:57:10.057Z", "fdContentType": "text\/plain", "fdSize": "108", "fdType": "file", "fdData": "PEZpbGVEYXRhPjxmZFR5cGU+ZmlsZTwvZmRUeXBlPjxmZERhdGE+U0dWc2JHOHNJR0ZuWVdsdUlRPT0KPC9mZERhdGE+PGZkQXJjaGl2ZT5mYWxzZTwvZmRBcmNoaXZlPjwvRmlsZURhdGE+"} 66 | ] 67 | } 68 | """ 69 | 70 | # These were grabbed from the live cloud 71 | GET_HOME_RESULT = '{\n "resultTotalRows": "3",\n "requestedStartRow": "0",\n "resultSize": "3",\n "requestedSize": "1000",\n "remainingSize": "0",\n "items": [\n{ "id": { "fdPath": "\\/db\\/CUS0000033_Spectrum_Design_Solutions__Paul_Osborne\\/", "fdName": "00000000-00000000-0004F3FF-FF027D8C"}, "cstId": "304", "fdCreatedDate": "2011-10-13T21:21:58.110Z", "fdLastModifiedDate": "2011-10-13T21:21:58.110Z", "fdContentType": "application\\/xml", "fdSize": "0", "fdType": "directory"},\n{ "id": { "fdPath": "\\/db\\/CUS0000033_Spectrum_Design_Solutions__Paul_Osborne\\/", "fdName": "00000000-00000000-080027FF-FFB1A2C2"}, "cstId": "304", "fdCreatedDate": "2012-01-16T02:22:16.510Z", "fdLastModifiedDate": "2012-01-16T02:22:16.510Z", "fdContentType": "application\\/xml", "fdSize": "0", "fdType": "directory"},\n{ "id": { "fdPath": "\\/db\\/CUS0000033_Spectrum_Design_Solutions__Paul_Osborne\\/", "fdName": "test_dir"}, "cstId": "304", "fdCreatedDate": "2014-07-13T04:37:16.287Z", "fdLastModifiedDate": "2014-07-13T04:37:16.287Z", "fdContentType": "application\\/xml", "fdSize": "0", "fdType": "directory"}\n ]\n }\n' 72 | GET_DIR1_RESULT = '{\n "resultTotalRows": "0",\n "requestedStartRow": "0",\n "resultSize": "0",\n "requestedSize": "1000",\n "remainingSize": "0",\n "items": [\n ]\n }\n' 73 | GET_DIR2_RESULT = '{\n "resultTotalRows": "0",\n "requestedStartRow": "0",\n "resultSize": "0",\n "requestedSize": "1000",\n "remainingSize": "0",\n "items": [\n ]\n }\n' 74 | GET_DIR3_RESULT = '{\n "resultTotalRows": "1",\n "requestedStartRow": "0",\n "resultSize": "1",\n "requestedSize": "1000",\n "remainingSize": "0",\n "items": [\n{ "id": { "fdPath": "\\/db\\/CUS0000033_Spectrum_Design_Solutions__Paul_Osborne\\/test_dir\\/", "fdName": "test_file.txt"}, "cstId": "304", "fdCreatedDate": "2014-07-13T04:37:16.283Z", "fdLastModifiedDate": "2014-07-21T06:14:13.550Z", "fdContentType": "text\\/plain", "fdSize": "149", "fdType": "file"}\n ]\n }\n' 75 | 76 | # This includes embedded data 77 | GET_WITH_EMBED = '{\n "resultTotalRows": "1",\n ' \ 78 | '"requestedStartRow": "0",\n ' \ 79 | '"resultSize": "1",\n ' \ 80 | '"requestedSize": "1000",\n ' \ 81 | '"remainingSize": "0",\n ' \ 82 | '"items": ' \ 83 | '[\n{ ' \ 84 | '"id": { ' \ 85 | '"fdPath": "\\/db\\/CUS0000033_Spectrum_Design_Solutions__Paul_Osborne\\/test_dir\\/", ' \ 86 | '"fdName": "test_file.txt"}, ' \ 87 | '"cstId": "304", ' \ 88 | '"fdCreatedDate": "2014-07-13T04:37:16.283Z", ' \ 89 | '"fdLastModifiedDate": "2014-07-21T07:30:15.383Z", ' \ 90 | '"fdContentType": "text\\/plain", ' \ 91 | '"fdSize": "149", ' \ 92 | '"fdType": "file", ' \ 93 | '"fdData": "PEZpbGVEYXRhPjxmZENvbnRlbnRUeXBlPnRleHQvcGxhaW48L2ZkQ29udGVudFR5cGU+PGZkVHlwZT5maWxlPC9mZFR5cGU+PGZkRGF0YT5TR1ZzYkd4dkxDQjNiM0pzWkNFPQo8L2ZkRGF0YT48ZmRBcmNoaXZlPmZhbHNlPC9mZEFyY2hpdmU+PC9GaWxlRGF0YT4="}\n ]\n }\n' 94 | 95 | 96 | class TestFileData(HttpTestBase): 97 | def test_get_filedata_simple(self): 98 | self.prepare_response("GET", "/ws/FileData", GET_FILEDATA_SIMPLE) 99 | objects = list(self.dc.filedata.get_filedata()) 100 | self.assertEqual(len(objects), 2) 101 | 102 | def test_get_filedata_paged(self): 103 | self.prepare_response("GET", "/ws/FileData", GET_FILEDATA_PAGE1) 104 | gen = self.dc.filedata.get_filedata(page_size=1) 105 | obj1 = six.next(gen) 106 | self.prepare_response("GET", "/ws/FileData", GET_FILEDATA_PAGE2) 107 | obj2 = six.next(gen) 108 | self.assertRaises(StopIteration, six.next, gen) 109 | self.assertEqual(obj1.get_name(), "test_file.txt") 110 | self.assertEqual(obj2.get_name(), "test_file2.txt") 111 | 112 | def test_write_file_simple(self): 113 | self.prepare_response("PUT", "/ws/FileData/test/path/test.txt", "", status=200) 114 | data = six.b(''.join(map(chr, range(255)))) 115 | self.dc.filedata.write_file( 116 | path="test/path", 117 | name="test.txt", 118 | data=data, 119 | content_type="application/binary", 120 | archive=True 121 | ) 122 | req = self._get_last_request() 123 | root = ElementTree.fromstring(req.body) 124 | self.assertEqual(root.find("fdContentType").text, "application/binary") 125 | self.assertEqual(root.find("fdType").text, "file") 126 | fd_data = root.find("fdData").text 127 | self.assertEqual(base64.decodestring(six.b(fd_data)), data) 128 | self.assertEqual(root.find("fdArchive").text, "true") 129 | 130 | def test_delete_path(self): 131 | self.prepare_response("DELETE", "/ws/FileData/test", "") 132 | self.dc.filedata.delete_file("/test") 133 | req = self._get_last_request() 134 | self.assertEqual(req.method, "DELETE") 135 | self.assertEqual(req.path, "/ws/FileData/test") 136 | 137 | def test_walk(self): 138 | self.prepare_response("GET", "/ws/FileData", GET_HOME_RESULT) 139 | gen = self.dc.filedata.walk() 140 | dirpath, dirnames, filenames = six.next(gen) 141 | self.assertEqual(dirpath, "~/") 142 | self.assertEqual(len(dirnames), 3) 143 | self.assertEqual([x.get_full_path() for x in dirnames], [ 144 | '/db/CUS0000033_Spectrum_Design_Solutions__Paul_Osborne/00000000-00000000-0004F3FF-FF027D8C', 145 | '/db/CUS0000033_Spectrum_Design_Solutions__Paul_Osborne/00000000-00000000-080027FF-FFB1A2C2', 146 | '/db/CUS0000033_Spectrum_Design_Solutions__Paul_Osborne/test_dir']) 147 | self.assertEqual(filenames, []) 148 | 149 | # Dir 1 150 | self.prepare_response("GET", "/ws/FileData", GET_DIR1_RESULT) 151 | dirpath, dirnames, filenames = six.next(gen) 152 | self.assertEqual(dirpath, 153 | "/db/CUS0000033_Spectrum_Design_Solutions__Paul_Osborne/00000000-00000000-0004F3FF-FF027D8C") 154 | self.assertEqual(dirnames, []) 155 | self.assertEqual(filenames, []) 156 | 157 | # Dir 2 158 | self.prepare_response("GET", "/ws/FileData", GET_DIR2_RESULT) 159 | dirpath, dirnames, filenames = six.next(gen) 160 | self.assertEqual(dirpath, 161 | "/db/CUS0000033_Spectrum_Design_Solutions__Paul_Osborne/00000000-00000000-080027FF-FFB1A2C2") 162 | self.assertEqual(dirnames, []) 163 | self.assertEqual(filenames, []) 164 | 165 | # Dir 3 166 | self.prepare_response("GET", "/ws/FileData", GET_DIR3_RESULT) 167 | dirpath, dirnames, filenames = six.next(gen) 168 | self.assertEqual(dirpath, "/db/CUS0000033_Spectrum_Design_Solutions__Paul_Osborne/test_dir") 169 | self.assertEqual(dirnames, []) 170 | self.assertEqual(len(filenames), 1) 171 | f = filenames[0] 172 | self.assertEqual(f.get_full_path(), 173 | "/db/CUS0000033_Spectrum_Design_Solutions__Paul_Osborne/test_dir/test_file.txt") 174 | 175 | 176 | class TestFileDataObject(HttpTestBase): 177 | 178 | def test_file_delete(self): 179 | self.prepare_response("GET", "/ws/FileData", GET_FILEDATA_SIMPLE) 180 | objects = list(self.dc.filedata.get_filedata()) 181 | self.assertEqual(len(objects), 2) 182 | obj = objects[0] 183 | self.assertEqual(obj.get_full_path(), "/db/blah/test.txt") 184 | 185 | self.prepare_response("DELETE", "/ws/FileData/db/blah/test.txt", "") 186 | obj.delete() 187 | req = self._get_last_request() 188 | self.assertEqual(req.method, "DELETE") 189 | self.assertEqual(req.path, "/ws/FileData/db/blah/test.txt") 190 | 191 | def test_file_metadata_access(self): 192 | self.prepare_response("GET", "/ws/FileData", GET_FILEDATA_SIMPLE) 193 | objects = list(self.dc.filedata.get_filedata()) 194 | self.assertEqual(len(objects), 2) 195 | obj = objects[0] 196 | self.assertEqual(obj.get_path(), "/db/blah/") 197 | self.assertEqual(obj.get_name(), "test.txt") 198 | self.assertEqual(obj.get_type(), "file") 199 | self.assertEqual(obj.get_content_type(), "application/binary") 200 | self.assertEqual(obj.get_last_modified_date(), 201 | datetime.datetime(2014, 7, 20, 18, 46, 45, 123000, tzinfo=tzutc())) 202 | self.assertEqual(obj.get_created_date(), 203 | datetime.datetime(2014, 7, 20, 18, 46, 45, 123000, tzinfo=tzutc())) 204 | self.assertEqual(obj.get_customer_id(), "1234") 205 | self.assertEqual(obj.get_full_path(), "/db/blah/test.txt") 206 | self.assertEqual(obj.get_size(), 1234) 207 | self.assertEqual(obj.get_data(), six.b("A man a plan a canal panama")) 208 | 209 | def test_directory_metadata_access(self): 210 | self.prepare_response("GET", "/ws/FileData", GET_FILEDATA_SIMPLE) 211 | objects = list(self.dc.filedata.get_filedata()) 212 | self.assertEqual(len(objects), 2) 213 | obj = objects[1] 214 | self.assertEqual(obj.get_path(), "/db/blah/") 215 | self.assertEqual(obj.get_name(), "") 216 | self.assertEqual(obj.get_type(), "directory") 217 | self.assertEqual(obj.get_content_type(), "application/xml") 218 | self.assertEqual(obj.get_last_modified_date(), 219 | datetime.datetime(2014, 7, 20, 18, 46, 45, 123000, tzinfo=tzutc())) 220 | self.assertEqual(obj.get_created_date(), 221 | datetime.datetime(2014, 7, 20, 18, 46, 45, 123000, tzinfo=tzutc())) 222 | self.assertEqual(obj.get_customer_id(), "1234") 223 | self.assertEqual(obj.get_full_path(), "/db/blah/") 224 | self.assertEqual(obj.get_size(), 0) 225 | self.assertEqual(obj.get_data(), None) 226 | 227 | 228 | if __name__ == "__main__": 229 | unittest.main() 230 | -------------------------------------------------------------------------------- /devicecloud/test/unit/test_monitor.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | # 5 | # Copyright (c) 2015-2018 Digi International Inc. 6 | 7 | from devicecloud.monitor import MON_TOPIC_ATTR, MON_TRANSPORT_TYPE_ATTR 8 | from devicecloud.test.unit.test_utilities import HttpTestBase 9 | import six 10 | 11 | CREATE_TCP_MONITOR_GOOD_REQUEST = """\ 12 | 13 | topA,topB 14 | 10 15 | json 16 | tcp 17 | gzip 18 | 19 | """ 20 | 21 | CREATE_HTTP_MONITOR_GOOD_REQUEST = """\ 22 | 23 | topA,topB 24 | 1 25 | json 26 | http 27 | http://digi.com 28 | None 29 | PUT 30 | 0 31 | 0 32 | none 33 | 34 | """ 35 | 36 | CREATE_MONITOR_GOOD_RESPONSE = """\ 37 | 38 | 39 | Monitor/178008 40 | 41 | """ 42 | 43 | GET_TCP_MONITOR_SINGLE_FOUND = """\ 44 | { 45 | "resultTotalRows": "1", 46 | "requestedStartRow": "0", 47 | "resultSize": "1", 48 | "requestedSize": "1000", 49 | "remainingSize": "0", 50 | "items": [ 51 | { 52 | "monId": "178007", 53 | "cstId": "7603", 54 | "monTopic": "DeviceCore,FileDataCore,FileData,DataPoint", 55 | "monTransportType": "tcp", 56 | "monFormatType": "json", 57 | "monBatchSize": "1", 58 | "monCompression": "zlib", 59 | "monStatus": "INACTIVE", 60 | "monBatchDuration": "10" 61 | } 62 | ] 63 | } 64 | """ 65 | 66 | GET_HTTP_MONITOR_SINGLE_FOUND = """\ 67 | { 68 | "resultTotalRows": "1", 69 | "requestedStartRow": "0", 70 | "resultSize": "1", 71 | "requestedSize": "1000", 72 | "remainingSize": "0", 73 | "items": [ 74 | { 75 | "monId": "178007", 76 | "cstId": "7603", 77 | "monTopic": "DeviceCore,FileDataCore,FileData,DataPoint", 78 | "monTransportType": "http", 79 | "monFormatType": "json", 80 | "monBatchSize": "1", 81 | "monCompression": "none", 82 | "monStatus": "INACTIVE", 83 | "monBatchDuration": "0" 84 | } 85 | ] 86 | } 87 | """ 88 | 89 | GET_TCP_MONITOR_METADTATA = """\ 90 | { 91 | "resultTotalRows": "1", 92 | "requestedStartRow": "0", 93 | "resultSize": "1", 94 | "requestedSize": "1000", 95 | "remainingSize": "0", 96 | "items": [ 97 | { 98 | "monId": "178007", 99 | "cstId": "7603", 100 | "monTopic": "DeviceCore,FileDataCore,FileData,DataPoint", 101 | "monTransportType": "tcp", 102 | "monFormatType": "json", 103 | "monBatchSize": "1", 104 | "monCompression": "zlib", 105 | "monStatus": "INACTIVE", 106 | "monBatchDuration": "10" 107 | } 108 | ] 109 | } 110 | """ 111 | 112 | GET_HTTP_MONITOR_METADTATA = """\ 113 | { 114 | "resultTotalRows": "1", 115 | "requestedStartRow": "0", 116 | "resultSize": "1", 117 | "requestedSize": "1000", 118 | "remainingSize": "0", 119 | "items": [ 120 | { 121 | "monId": "178007", 122 | "cstId": "7603", 123 | "monTopic": "DeviceCore,FileDataCore,FileData,DataPoint", 124 | "monTransportType": "http", 125 | "monFormatType": "json", 126 | "monBatchSize": "1", 127 | "monCompression": "none", 128 | "monStatus": "INACTIVE", 129 | "monBatchDuration": "0" 130 | } 131 | ] 132 | } 133 | """ 134 | 135 | GET_MONITOR_MULTIPLE_FOUND = """\ 136 | { 137 | "resultTotalRows": "2", 138 | "requestedStartRow": "0", 139 | "resultSize": "2", 140 | "requestedSize": "1000", 141 | "remainingSize": "0", 142 | "items": [ 143 | { 144 | "monId": "178007", 145 | "cstId": "7603", 146 | "monTopic": "DeviceCore,FileDataCore,FileData,DataPoint", 147 | "monTransportType": "tcp", 148 | "monFormatType": "json", 149 | "monBatchSize": "1", 150 | "monCompression": "zlib", 151 | "monStatus": "INACTIVE", 152 | "monBatchDuration": "10" 153 | }, 154 | { 155 | "monId": "178007", 156 | "cstId": "7603", 157 | "monTopic": "DeviceCore,FileDataCore,FileData,DataPoint", 158 | "monTransportType": "tcp", 159 | "monFormatType": "json", 160 | "monBatchSize": "1", 161 | "monCompression": "zlib", 162 | "monStatus": "INACTIVE", 163 | "monBatchDuration": "10" 164 | }, 165 | { 166 | "monId": "178007", 167 | "cstId": "7603", 168 | "monTopic": "DeviceCore,FileDataCore,FileData,DataPoint", 169 | "monTransportType": "http", 170 | "monFormatType": "json", 171 | "monBatchSize": "1", 172 | "monCompression": "none", 173 | "monStatus": "INACTIVE", 174 | "monBatchDuration": "0" 175 | } 176 | ] 177 | } 178 | """ 179 | 180 | GET_MONITOR_NONE_FOUND = """\ 181 | { 182 | "resultTotalRows": "0", 183 | "requestedStartRow": "0", 184 | "resultSize": "0", 185 | "requestedSize": "1000", 186 | "remainingSize": "0", 187 | "items": [] 188 | } 189 | """ 190 | 191 | 192 | class TestMonitorAPI(HttpTestBase): 193 | 194 | def test_create_tcp_monitor(self): 195 | self.prepare_response("POST", "/ws/Monitor", data=CREATE_MONITOR_GOOD_RESPONSE) 196 | mon = self.dc.monitor.create_tcp_monitor(['topA', 'topB'], batch_size=10, batch_duration=0, 197 | compression='gzip', format_type='json') 198 | self.assertEqual(self._get_last_request().body, six.b(CREATE_TCP_MONITOR_GOOD_REQUEST)) 199 | self.assertEqual(mon.get_id(), 178008) 200 | 201 | def test_create_http_monitor(self): 202 | self.prepare_response("POST", "/ws/Monitor", data=CREATE_MONITOR_GOOD_RESPONSE) 203 | mon = self.dc.monitor.create_http_monitor(['topA', 'topB'], 'http://digi.com', transport_token=None, 204 | transport_method='PUT', connect_timeout=0, response_timeout=0, 205 | batch_size=1, batch_duration=0, compression='none', 206 | format_type='json') 207 | self.assertEqual(self._get_last_request().body, six.b(CREATE_HTTP_MONITOR_GOOD_REQUEST)) 208 | self.assertEqual(mon.get_id(), 178008) 209 | 210 | def test_get_tcp_monitors(self): 211 | self.prepare_response("GET", "/ws/Monitor", data=GET_TCP_MONITOR_SINGLE_FOUND) 212 | mons = list(self.dc.monitor.get_monitors((MON_TOPIC_ATTR == "DeviceCore") & 213 | (MON_TRANSPORT_TYPE_ATTR == "tcp"))) 214 | self.assertEqual(len(mons), 1) 215 | mon = mons[0] 216 | self.assertEqual(mon.get_id(), 178007) 217 | self.assertEqual(self._get_last_request_params(), { 218 | 'condition': "monTopic='DeviceCore' and monTransportType='tcp'", 219 | 'start': '0', 220 | 'size': '1000' 221 | }) 222 | 223 | def test_get_http_monitors(self): 224 | self.prepare_response("GET", "/ws/Monitor", data=GET_TCP_MONITOR_SINGLE_FOUND) 225 | mons = list(self.dc.monitor.get_monitors((MON_TOPIC_ATTR == "DeviceCore") & 226 | (MON_TRANSPORT_TYPE_ATTR == "http"))) 227 | self.assertEqual(len(mons), 1) 228 | mon = mons[0] 229 | self.assertEqual(mon.get_id(), 178007) 230 | self.assertEqual(self._get_last_request_params(), { 231 | 'condition': "monTopic='DeviceCore' and monTransportType='http'", 232 | 'start': '0', 233 | 'size': '1000' 234 | }) 235 | 236 | def test_tcp_get_monitor_present(self): 237 | self.prepare_response("GET", "/ws/Monitor", data=GET_TCP_MONITOR_SINGLE_FOUND) 238 | mon = self.dc.monitor.get_monitor(['DeviceCore', 'FileDataCore', 'FileData', 'DataPoint']) 239 | self.assertEqual(mon.get_id(), 178007) 240 | self.assertEqual(self._get_last_request_params(), { 241 | 'condition': "monTopic='DeviceCore,FileDataCore,FileData,DataPoint'", 242 | 'start': '0', 243 | 'size': '1000' 244 | }) 245 | 246 | def test_http_get_monitor_present(self): 247 | self.prepare_response("GET", "/ws/Monitor", data=GET_HTTP_MONITOR_SINGLE_FOUND) 248 | mon = self.dc.monitor.get_monitor(['DeviceCore', 'FileDataCore', 'FileData', 'DataPoint']) 249 | self.assertEqual(mon.get_id(), 178007) 250 | self.assertEqual(self._get_last_request_params(), { 251 | 'condition': "monTopic='DeviceCore,FileDataCore,FileData,DataPoint'", 252 | 'start': '0', 253 | 'size': '1000' 254 | }) 255 | 256 | def test_get_monitor_multiple(self): 257 | # Should just pick the first result (currently), so results are the same as ever 258 | self.prepare_response("GET", "/ws/Monitor", data=GET_MONITOR_MULTIPLE_FOUND) 259 | mon = self.dc.monitor.get_monitor(['DeviceCore', 'FileDataCore', 'FileData', 'DataPoint']) 260 | self.assertEqual(mon.get_id(), 178007) 261 | self.assertEqual(self._get_last_request_params(), { 262 | 'condition': "monTopic='DeviceCore,FileDataCore,FileData,DataPoint'", 263 | 'start': '0', 264 | 'size': '1000' 265 | }) 266 | 267 | def test_get_monitor_does_not_exist(self): 268 | # Should just pick the first result (currently), so results are the same as ever 269 | self.prepare_response("GET", "/ws/Monitor", data=GET_MONITOR_NONE_FOUND) 270 | mon = self.dc.monitor.get_monitor(['DeviceCore', 'FileDataCore', 'FileData', 'DataPoint']) 271 | self.assertEqual(mon, None) 272 | 273 | 274 | class TestDeviceCloudMonitor(HttpTestBase): 275 | 276 | def setUp(self): 277 | HttpTestBase.setUp(self) 278 | self.prepare_response("GET", "/ws/Monitor", data=GET_TCP_MONITOR_SINGLE_FOUND) 279 | mon = self.dc.monitor.get_monitor(['DeviceCore', 'FileDataCore', 'FileData', 'DataPoint']) 280 | self.mon = mon 281 | 282 | def test_get_tcp_metadata(self): 283 | self.prepare_response("GET", "/ws/Monitor/178007", data=GET_TCP_MONITOR_METADTATA) 284 | self.assertEqual(self.mon.get_metadata(), { 285 | "monId": "178007", 286 | "cstId": "7603", 287 | "monTopic": "DeviceCore,FileDataCore,FileData,DataPoint", 288 | "monTransportType": "tcp", 289 | "monFormatType": "json", 290 | "monBatchSize": "1", 291 | "monCompression": "zlib", 292 | "monStatus": "INACTIVE", 293 | "monBatchDuration": "10" 294 | }) 295 | 296 | def test_delete(self): 297 | self.prepare_response("DELETE", "/ws/Monitor/178007") 298 | self.mon.delete() 299 | req = self._get_last_request() 300 | self.assertEqual(req.method, "DELETE") 301 | self.assertEqual(req.path, "/ws/Monitor/178007") 302 | 303 | def test_get_id(self): 304 | self.assertEqual(self.mon.get_id(), 178007) 305 | -------------------------------------------------------------------------------- /devicecloud/test/unit/test_monitor_tcp.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | # 5 | # Copyright (c) 2015-2018 Digi International Inc. 6 | 7 | from devicecloud.monitor_tcp import TCPClientManager 8 | from devicecloud.test.unit.test_utilities import HttpTestBase 9 | 10 | 11 | class TestTCPClientManager(HttpTestBase): 12 | 13 | # NOTE: currently only integration tests exist to test several parts of 14 | # the basic device cloud push client functionality for historical reasons. 15 | # In the future, it would be nice to extended the unit test coverage 16 | # for this code. 17 | 18 | def setUp(self): 19 | HttpTestBase.setUp(self) 20 | self.client_manager = TCPClientManager(self.dc.get_connection()) 21 | 22 | def test_hostname(self): 23 | self.assertEqual(self.client_manager.hostname, "devicecloud.digi.com") 24 | 25 | def test_username(self): 26 | self.assertEqual(self.client_manager.username, "user") 27 | 28 | def test_password(self): 29 | self.assertEqual(self.client_manager.password, "pass") 30 | -------------------------------------------------------------------------------- /devicecloud/test/unit/test_sci.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | # 5 | # Copyright (c) 2015-2018 Digi International Inc. 6 | 7 | import unittest 8 | 9 | import httpretty 10 | import mock 11 | import six 12 | import re 13 | import xml.etree.ElementTree as ET 14 | 15 | from devicecloud import DeviceCloud 16 | from devicecloud.sci import DeviceTarget, GroupTarget, AsyncRequestProxy, ServerCommandInterfaceAPI 17 | from devicecloud.test.unit.test_utilities import HttpTestBase 18 | 19 | 20 | EXAMPLE_SCI_DEVICE_NOT_CONNECTED = """\ 21 | Device Not Connected 22 | """ 23 | 24 | EXAMPLE_SCI_BAD_DEVICE = """\ 25 | 26 | 27 | 28 | 29 | Invalid target. Device not found. 30 | 31 | 32 | Invalid SCI request. No valid targets found. 33 | 34 | 35 | """ 36 | 37 | EXAMPLE_ASYNC_SCI_DEVICE_NOT_CONNECTED = """\ 38 | completeDevice Not Connected 39 | """ 40 | 41 | EXAMPLE_ASYNC_SCI_INCOMPLETE = """\ 42 | in_progress 43 | """ 44 | 45 | EXAMPLE_SCI_REQUEST_PAYLOAD = """\ 46 | 47 | 48 | 49 | 50 | 51 | """ 52 | 53 | EXAMPLE_SCI_REQUEST_RESPONSE = """\ 54 | 55 | 56 | 57 | 58 | 59 | 60 | running 61 | 62 | 63 | 64 | 65 | 66 | 67 | """ 68 | 69 | EXAMPLE_ASYNC_SCI_RESPONSE = """\ 70 | 71 | 72 | 133225503 73 | 74 | 75 | """ 76 | 77 | EXAMPLE_UPDATE_FIRMWARE_INVALID_ATTRIBUTE_REQUEST_PAYLOAD = """ 78 | aHNxcAbAADUct1cAAACAHEBAAAEABEAwAIBAAQAAACOFFzU 79 | """ 80 | 81 | EXAMPLE_UPDATE_FIRMWARE_INVALID_ATTRIBUTE_RESPONSE = """\ 82 | 83 | Default target not availabe, specify a target number of filename 84 | 85 | 86 | """ 87 | 88 | class TestSCI(HttpTestBase): 89 | def _prepare_sci_response(self, response, status=200): 90 | self.prepare_response("POST", "/ws/sci", response, status) 91 | 92 | def test_sci_successful_error(self): 93 | self._prepare_sci_response(EXAMPLE_SCI_DEVICE_NOT_CONNECTED) 94 | self.dc.get_sci_api().send_sci( 95 | operation="send_message", 96 | target=DeviceTarget("00000000-00000000-00409DFF-FF58175B"), 97 | payload="") 98 | self.assertEqual(httpretty.last_request().body, 99 | six.b('' 100 | '' 101 | '' 102 | '' 103 | '' 104 | '' 105 | '' 106 | '')) 107 | 108 | def test_sci_successful_group_target(self): 109 | self._prepare_sci_response(EXAMPLE_SCI_DEVICE_NOT_CONNECTED) 110 | self.dc.get_sci_api().send_sci( 111 | operation="send_message", 112 | target=GroupTarget("TestGroup"), 113 | payload="") 114 | self.assertEqual(httpretty.last_request().body, 115 | six.b('' 116 | '' 117 | '' 118 | '' 119 | '' 120 | '' 121 | '' 122 | '')) 123 | 124 | def test_sci_no_parameters(self): 125 | self._prepare_sci_response(EXAMPLE_SCI_REQUEST_RESPONSE) 126 | self.dc.get_sci_api().send_sci( 127 | operation="send_message", 128 | target=DeviceTarget('00000000-00000000-00409dff-ffaabbcc'), 129 | payload=EXAMPLE_SCI_REQUEST_PAYLOAD) 130 | request = httpretty.last_request().body.decode('utf8') 131 | # Strip white space from lines and concatenate request 132 | request = ''.join([line.strip() for line in request.splitlines()]) 133 | self.assertEqual(request, 134 | six.u('' 135 | '' 136 | '' 137 | '' 138 | '' 139 | '' 140 | '' 141 | '' 142 | '' 143 | '' 144 | '' 145 | '')) 146 | 147 | def test_sci_with_parameters(self): 148 | self._prepare_sci_response(EXAMPLE_SCI_REQUEST_RESPONSE) 149 | self.dc.get_sci_api().send_sci( 150 | operation="send_message", 151 | target=DeviceTarget('00000000-00000000-00409dff-ffaabbcc'), 152 | payload=EXAMPLE_SCI_REQUEST_PAYLOAD, 153 | reply="all", 154 | synchronous=True, 155 | sync_timeout=42, 156 | cache=False, 157 | allow_offline=True, 158 | wait_for_reconnect=True, 159 | ) 160 | request = httpretty.last_request().body.decode('utf8') 161 | # Verify attributes exist in 162 | expected_attrib = { 163 | "reply": "all", 164 | "synchronous": "true", 165 | "syncTimeout": "42", 166 | "cache": "false", 167 | "allowOffline": "true", 168 | "waitForReconnect": "true", 169 | } 170 | request_e = ET.fromstring(request) 171 | send_message_e = request_e.find('./send_message') 172 | self.assertEqual(expected_attrib, send_message_e.attrib) 173 | # Strip white space from lines and concatenate request 174 | request = ''.join([line.strip() for line in request.splitlines()]) 175 | # Replace from request with one without parameters so the final check can be done 176 | match = re.search('', request) 177 | request = request[:match.start()] + '' + request[match.end():] 178 | self.assertEqual(request, 179 | six.u('' 180 | '' 181 | '' 182 | '' 183 | '' 184 | '' 185 | '' 186 | '' 187 | '' 188 | '' 189 | '' 190 | '')) 191 | 192 | def test_sci_update_firmware_attribute(self): 193 | 194 | self._prepare_sci_response(EXAMPLE_UPDATE_FIRMWARE_INVALID_ATTRIBUTE_RESPONSE) 195 | self.dc.get_sci_api().send_sci( 196 | operation="update_firmware", 197 | attribute="filename=\"abcd.bin\"", 198 | target=DeviceTarget('00000000-00000000-00409dff-ffaabbcc'), 199 | payload=EXAMPLE_UPDATE_FIRMWARE_INVALID_ATTRIBUTE_REQUEST_PAYLOAD) 200 | 201 | request = httpretty.last_request().body.decode('utf8') 202 | request = ''.join([line.strip() for line in request.splitlines()]) 203 | self.assertEqual(request, 204 | six.u('' 205 | '' 206 | '' 207 | '' 208 | '' 209 | 'aHNxcAbAADUct1cAAACAHEBAAAEABEAwAIBAAQAAACOFFzU' 210 | '' 211 | '')) 212 | 213 | 214 | class TestGetAsync(HttpTestBase): 215 | def test_sci_get_async(self): 216 | self.prepare_response("GET", "/ws/sci/123", EXAMPLE_ASYNC_SCI_DEVICE_NOT_CONNECTED, 200) 217 | resp = self.dc.get_sci_api().get_async_job(123) 218 | self.assertEqual(resp.status_code, 200) 219 | self.assertEqual(resp.content, six.b(EXAMPLE_ASYNC_SCI_DEVICE_NOT_CONNECTED)) 220 | 221 | 222 | class TestAsyncProxy(HttpTestBase): 223 | def setUp(self): 224 | HttpTestBase.setUp(self) 225 | self.fake_conn = mock.MagicMock() 226 | 227 | def test_ctor(self): 228 | t = AsyncRequestProxy(123, self.fake_conn) 229 | self.assertEqual(t.job_id, 123) 230 | self.assertIs(t.response, None) 231 | 232 | def test_completed_false(self): 233 | self.prepare_response("GET", "/ws/sci/123", EXAMPLE_ASYNC_SCI_INCOMPLETE, 200) 234 | t = AsyncRequestProxy(123, self.dc.get_sci_api()._conn) 235 | self.assertIs(t.completed, False) 236 | self.assertIs(t.response, None) 237 | 238 | def test_completed_true(self): 239 | self.prepare_response("GET", "/ws/sci/123", EXAMPLE_ASYNC_SCI_DEVICE_NOT_CONNECTED, 200) 240 | t = AsyncRequestProxy(123, self.dc.get_sci_api()._conn) 241 | self.assertIs(t.completed, True) 242 | self.assertEqual(t.response, six.b(EXAMPLE_ASYNC_SCI_DEVICE_NOT_CONNECTED)) 243 | 244 | def test_completed_already(self): 245 | self.prepare_response("GET", "/ws/sci/123", EXAMPLE_ASYNC_SCI_DEVICE_NOT_CONNECTED, 200) 246 | t = AsyncRequestProxy(123, self.dc.get_sci_api()._conn) 247 | t.response = EXAMPLE_ASYNC_SCI_DEVICE_NOT_CONNECTED 248 | self.assertIs(t.completed, True) 249 | 250 | 251 | class TestSendSciAsync(HttpTestBase): 252 | @mock.patch.object(ServerCommandInterfaceAPI, "send_sci") 253 | def test_bad_resp(self, fake_send_sci): 254 | fake_resp = mock.MagicMock() 255 | fake_resp.status_code = 400 256 | fake_resp.reason = "OK" 257 | fake_resp.content = EXAMPLE_SCI_BAD_DEVICE 258 | fake_send_sci.return_value = fake_resp 259 | resp = self.dc.get_sci_api().send_sci_async("send_message", DeviceTarget('00000000-00000000-00409dff-ffaabbcc'), EXAMPLE_SCI_REQUEST_PAYLOAD) 260 | self.assertIs(resp, None) 261 | 262 | @mock.patch.object(ServerCommandInterfaceAPI, "send_sci") 263 | def test_resp_parse(self, fake_send_sci): 264 | fake_resp = mock.MagicMock() 265 | fake_resp.status_code = 200 266 | fake_resp.reason = "OK" 267 | fake_resp.content = EXAMPLE_ASYNC_SCI_RESPONSE 268 | fake_send_sci.return_value = fake_resp 269 | resp = self.dc.get_sci_api().send_sci_async("send_message", DeviceTarget('00000000-00000000-00409dff-ffaabbcc'), EXAMPLE_SCI_REQUEST_PAYLOAD) 270 | self.assertEqual(resp.job_id, 133225503) 271 | 272 | 273 | if __name__ == "__main__": 274 | unittest.main() 275 | -------------------------------------------------------------------------------- /devicecloud/test/unit/test_utilities.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | # 5 | # Copyright (c) 2015-2018 Digi International Inc. 6 | 7 | import unittest 8 | import json 9 | 10 | from devicecloud import DeviceCloud 11 | import httpretty 12 | import six.moves.urllib.parse as urllib_parse 13 | 14 | 15 | class HttpTestBase(unittest.TestCase): 16 | def setUp(self): 17 | httpretty.enable() 18 | # setup Device Cloud ping response 19 | self.prepare_response("GET", "/ws/DeviceCore?size=1", "", status=200) 20 | self.dc = DeviceCloud('user', 'pass') 21 | 22 | def tearDown(self): 23 | httpretty.disable() 24 | httpretty.reset() 25 | 26 | def _get_last_request(self): 27 | return httpretty.last_request() 28 | 29 | def _get_last_request_params(self): 30 | # Get the query params from the last request as a dictionary 31 | params = urllib_parse.parse_qs(urllib_parse.urlparse(self._get_last_request().path).query) 32 | return {k: v[0] for k, v in params.items()} # convert from list values to single-value 33 | 34 | def prepare_response(self, method, path, data=None, status=200, match_querystring=False, **kwargs): 35 | # TODO: 36 | # Should probably assert on more request headers and 37 | # respond with correct content type, etc. 38 | if data is not None: 39 | kwargs['body'] = data 40 | httpretty.register_uri(method, 41 | "https://devicecloud.digi.com{}".format(path), 42 | match_querystring=match_querystring, 43 | status=status, 44 | **kwargs) 45 | 46 | def prepare_json_response(self, method, path, data, status=200): 47 | self.prepare_response(method, path, json.dumps(data), status=status) 48 | -------------------------------------------------------------------------------- /devicecloud/test/unit/test_version.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | # 5 | # Copyright (c) 2015-2018 Digi International Inc. All rights reserved. 6 | 7 | import unittest 8 | 9 | from devicecloud.version import __version__ 10 | 11 | 12 | class TestVersion(unittest.TestCase): 13 | 14 | def test_version_format(self): 15 | self.assertTrue(len(__version__.split('.')) >= 3) 16 | -------------------------------------------------------------------------------- /devicecloud/test/unit/test_ws.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | # 5 | # Copyright (c) 2015-2018 Digi International Inc. All rights reserved. 6 | 7 | import unittest 8 | 9 | from devicecloud import DeviceCloudException 10 | from mock import MagicMock, patch 11 | 12 | from devicecloud.ws import WebServiceStub 13 | from devicecloud import DeviceCloudConnection 14 | 15 | 16 | class MockConnection(MagicMock): 17 | 18 | def get(self, *args, **kwargs): 19 | return (args, kwargs) 20 | 21 | def post(self, *args, **kwargs): 22 | return (args, kwargs) 23 | 24 | 25 | class LegacyAPIMemberInternalsTests(unittest.TestCase): 26 | 27 | def setUp(self): 28 | self.conn = MockConnection() 29 | self.stub = WebServiceStub(self.conn, '/ws') 30 | 31 | def test_path_building(self): 32 | test = self.stub.a.b.c 33 | self.assertEqual(test._path, "/ws/a/b/c") 34 | 35 | def test_method_access(self): 36 | res = self.stub.a.b.c.get() 37 | self.assertEqual(res[0], ("/ws/a/b/c", )) 38 | self.assertDictEqual(res[1], {}) 39 | 40 | def test_method_access_args_kwargs(self): 41 | res = self.stub.a.test.path.post("foo", bar="baz") 42 | self.assertEqual(res[0], ("/ws/a/test/path", "foo")) 43 | self.assertDictEqual(res[1], {"bar": "baz"}) 44 | 45 | 46 | if __name__ == '__main__': 47 | unittest.main() 48 | -------------------------------------------------------------------------------- /devicecloud/util.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | # 5 | # Copyright (c) 2015-2018 Digi International Inc. 6 | import datetime 7 | 8 | import arrow 9 | from arrow.parser import DateTimeParser, ParserError 10 | import six 11 | 12 | 13 | def conditional_write(strm, fmt, value, *args, **kwargs): 14 | """Write to stream using fmt and value if value is not None""" 15 | if value is not None: 16 | strm.write(fmt.format(value, *args, **kwargs)) 17 | 18 | 19 | def iso8601_to_dt(iso8601): 20 | """Given an ISO8601 string as returned by Device Cloud, convert to a datetime object""" 21 | # We could just use arrow.get() but that is more permissive than we actually want. 22 | # Internal (but still public) to arrow is the actual parser where we can be 23 | # a bit more specific 24 | parser = DateTimeParser() 25 | try: 26 | arrow_dt = arrow.Arrow.fromdatetime(parser.parse_iso(iso8601)) 27 | return arrow_dt.to('utc').datetime 28 | except ParserError as pe: 29 | raise ValueError("Provided was not a valid ISO8601 string: %r" % pe) 30 | 31 | 32 | def to_none_or_dt(input): 33 | """Convert ``input`` to either None or a datetime object 34 | 35 | If the input is None, None will be returned. 36 | If the input is a datetime object, it will be converted to a datetime 37 | object with UTC timezone info. If the datetime object is naive, then 38 | this method will assume the object is specified according to UTC and 39 | not local or some other timezone. 40 | If the input to the function is a string, this method will attempt to 41 | parse the input as an ISO-8601 formatted string. 42 | 43 | :param input: Input data (expected to be either str, None, or datetime object) 44 | :return: datetime object from input or None if already None 45 | :rtype: datetime or None 46 | 47 | """ 48 | if input is None: 49 | return input 50 | elif isinstance(input, datetime.datetime): 51 | arrow_dt = arrow.Arrow.fromdatetime(input, input.tzinfo or 'utc') 52 | return arrow_dt.to('utc').datetime 53 | if isinstance(input, six.string_types): 54 | # try to convert from ISO8601 55 | return iso8601_to_dt(input) 56 | else: 57 | raise TypeError("Not a string, NoneType, or datetime object") 58 | 59 | 60 | def validate_type(input, *types): 61 | """Raise TypeError if the type of ``input`` is one of the args 62 | 63 | If the input value is one of the types specified, just return 64 | the input value. 65 | 66 | """ 67 | if not isinstance(input, types): 68 | raise TypeError("Input expected to one of following types: %s" % (types, )) 69 | return input 70 | 71 | 72 | def isoformat(dt): 73 | """Return an ISO-8601 formatted string from the provided datetime object""" 74 | if not isinstance(dt, datetime.datetime): 75 | raise TypeError("Must provide datetime.datetime object to isoformat") 76 | 77 | if dt.tzinfo is None: 78 | raise ValueError("naive datetime objects are not allowed beyond the library boundaries") 79 | 80 | return dt.isoformat().replace("+00:00", "Z") # nicer to look at 81 | 82 | 83 | def dc_utc_timestamp_to_dt(dc_timestamp_in_milleseconds): 84 | """Return a UTC datetime object""" 85 | return arrow.Arrow.utcfromtimestamp(dc_timestamp_in_milleseconds / 1000).datetime 86 | -------------------------------------------------------------------------------- /devicecloud/version.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | # 5 | # Copyright (c) 2015-2018 Digi International Inc. 6 | 7 | 8 | __version__ = "0.5.10" 9 | -------------------------------------------------------------------------------- /devicecloud/ws.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | # 5 | # Copyright (c) 2015-2018 Digi International Inc. All rights reserved. 6 | import functools 7 | import inspect 8 | 9 | 10 | class WebServiceStub(object): 11 | """Provide a set of methods to directory access the web services API at a given path 12 | 13 | Web services stubs can be chained in order to build a path and, eventually, perform 14 | an operation on the end result. For instance, the following will perform 15 | a GET request on the path ``/some/base/with/some/added/stuff``:: 16 | 17 | WebServiceStub(conn, "/some/base").with.some.added.stuff.get() 18 | 19 | In the context of the this library, a more common example might be accessing 20 | a V1 API such as /ws/v1/devices. That would be done like this:: 21 | 22 | response = dc.ws.v1.devices.get() 23 | 24 | Where response would end up being a `requests Response object 25 | `. 26 | 27 | Any of the methods exposed by :class:`devicecloud.DeviceCloudConnection` may be 28 | called and the path for the stub will be passed as the first argument to 29 | the method with the same name in that class. 30 | 31 | """ 32 | 33 | def __init__(self, conn, path): 34 | self._conn = conn 35 | self._path = "/" + path if path[0] != '/' else path 36 | 37 | def __getattr__(self, attr): 38 | """We implement this method to provide the "builder" syntax""" 39 | conn_meth = getattr(self._conn, attr, None) 40 | if conn_meth is not None and inspect.ismethod(conn_meth): 41 | # If this is method on DeviceCloudConnection, then return a function bound to 42 | # that method that will be called with this stub's path 43 | @functools.wraps(conn_meth) 44 | def bound_cloud_connection_method(*args, **kwargs): 45 | return conn_meth(self._path, *args, **kwargs) 46 | return bound_cloud_connection_method 47 | 48 | # Otherwise, assume that specified attribute is another path and return 49 | # a new builder which is our path combined with the provided attribute 50 | return WebServiceStub(self._conn, "{}/{}".format(self._path, attr)) 51 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/python-devicecloud.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/python-devicecloud.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/python-devicecloud" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/python-devicecloud" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/_static/.keepme: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digidotcom/python-devicecloud/4940d6f6364a79900a12281c1b243f6c9d42638c/docs/_static/.keepme -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # python-devicecloud documentation build configuration file, created by 4 | # sphinx-quickstart on Thu Jul 3 10:03:27 2014. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | import sphinx_rtd_theme 18 | 19 | # If extensions (or modules to document with autodoc) are in another directory, 20 | # add these directories to sys.path here. If the directory is relative to the 21 | # documentation root, use os.path.abspath to make it absolute, like shown here. 22 | this_dir = os.path.dirname(__file__) 23 | sys.path.insert(0, os.path.abspath(os.path.join(this_dir, ".."))) 24 | 25 | import devicecloud 26 | 27 | # -- General configuration ------------------------------------------------ 28 | 29 | # If your documentation needs a minimal Sphinx version, state it here. 30 | #needs_sphinx = '1.0' 31 | 32 | # Add any Sphinx extension module names here, as strings. They can be 33 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 34 | # ones. 35 | extensions = [ 36 | 'sphinx.ext.autodoc', 37 | 'sphinx.ext.intersphinx' 38 | ] 39 | 40 | intersphinx_mapping = { 41 | 'python': ('http://docs.python.org/3', None), 42 | } 43 | 44 | # Add any paths that contain templates here, relative to this directory. 45 | templates_path = ['_templates'] 46 | 47 | # The suffix of source filenames. 48 | source_suffix = '.rst' 49 | 50 | # The encoding of source files. 51 | #source_encoding = 'utf-8-sig' 52 | 53 | # The master toctree document. 54 | master_doc = 'index' 55 | 56 | # General information about the project. 57 | project = u'python-devicecloud' 58 | copyright = u'2015-2018, Digi International Inc.' 59 | 60 | # The version info for the project you're documenting, acts as replacement for 61 | # |version| and |release|, also used in various other places throughout the 62 | # built documents. 63 | # 64 | # The short X.Y version. 65 | version = devicecloud.__version__ 66 | # The full version, including alpha/beta/rc tags. 67 | release = devicecloud.__version__ 68 | 69 | # The language for content autogenerated by Sphinx. Refer to documentation 70 | # for a list of supported languages. 71 | #language = None 72 | 73 | # There are two options for replacing |today|: either, you set today to some 74 | # non-false value, then it is used: 75 | #today = '' 76 | # Else, today_fmt is used as the format for a strftime call. 77 | #today_fmt = '%B %d, %Y' 78 | 79 | # List of patterns, relative to source directory, that match files and 80 | # directories to ignore when looking for source files. 81 | exclude_patterns = ['_build'] 82 | 83 | # The reST default role (used for this markup: `text`) to use for all 84 | # documents. 85 | #default_role = None 86 | 87 | # If true, '()' will be appended to :func: etc. cross-reference text. 88 | #add_function_parentheses = True 89 | 90 | # If true, the current module name will be prepended to all description 91 | # unit titles (such as .. function::). 92 | #add_module_names = True 93 | 94 | # If true, sectionauthor and moduleauthor directives will be shown in the 95 | # output. They are ignored by default. 96 | #show_authors = False 97 | 98 | # The name of the Pygments (syntax highlighting) style to use. 99 | pygments_style = 'sphinx' 100 | 101 | # A list of ignored prefixes for module index sorting. 102 | #modindex_common_prefix = [] 103 | 104 | # If true, keep warnings as "system message" paragraphs in the built documents. 105 | #keep_warnings = False 106 | 107 | 108 | # -- Options for HTML output ---------------------------------------------- 109 | 110 | # The theme to use for HTML and HTML Help pages. See the documentation for 111 | # a list of builtin themes. 112 | html_theme = 'sphinx_rtd_theme' 113 | 114 | # Theme options are theme-specific and customize the look and feel of a theme 115 | # further. For a list of options available for each theme, see the 116 | # documentation. 117 | #html_theme_options = {} 118 | 119 | # Add any paths that contain custom themes here, relative to this directory. 120 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path(), ] 121 | 122 | # The name for this set of Sphinx documents. If None, it defaults to 123 | # " v documentation". 124 | #html_title = None 125 | 126 | # A shorter title for the navigation bar. Default is the same as html_title. 127 | #html_short_title = None 128 | 129 | # The name of an image file (relative to this directory) to place at the top 130 | # of the sidebar. 131 | #html_logo = None 132 | 133 | # The name of an image file (within the static path) to use as favicon of the 134 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 135 | # pixels large. 136 | #html_favicon = None 137 | 138 | # Add any paths that contain custom static files (such as style sheets) here, 139 | # relative to this directory. They are copied after the builtin static files, 140 | # so a file named "default.css" will overwrite the builtin "default.css". 141 | html_static_path = ['_static'] 142 | 143 | # Add any extra paths that contain custom files (such as robots.txt or 144 | # .htaccess) here, relative to this directory. These files are copied 145 | # directly to the root of the documentation. 146 | #html_extra_path = [] 147 | 148 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 149 | # using the given strftime format. 150 | #html_last_updated_fmt = '%b %d, %Y' 151 | 152 | # If true, SmartyPants will be used to convert quotes and dashes to 153 | # typographically correct entities. 154 | #html_use_smartypants = True 155 | 156 | # Custom sidebar templates, maps document names to template names. 157 | #html_sidebars = {} 158 | 159 | # Additional templates that should be rendered to pages, maps page names to 160 | # template names. 161 | #html_additional_pages = {} 162 | 163 | # If false, no module index is generated. 164 | #html_domain_indices = True 165 | 166 | # If false, no index is generated. 167 | #html_use_index = True 168 | 169 | # If true, the index is split into individual pages for each letter. 170 | #html_split_index = False 171 | 172 | # If true, links to the reST sources are added to the pages. 173 | #html_show_sourcelink = True 174 | 175 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 176 | #html_show_sphinx = True 177 | 178 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 179 | #html_show_copyright = True 180 | 181 | # If true, an OpenSearch description file will be output, and all pages will 182 | # contain a tag referring to it. The value of this option must be the 183 | # base URL from which the finished HTML is served. 184 | #html_use_opensearch = '' 185 | 186 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 187 | #html_file_suffix = None 188 | 189 | # Output file base name for HTML help builder. 190 | htmlhelp_basename = 'python-deviceclouddoc' 191 | 192 | 193 | # -- Options for LaTeX output --------------------------------------------- 194 | 195 | latex_elements = { 196 | # The paper size ('letterpaper' or 'a4paper'). 197 | #'papersize': 'letterpaper', 198 | 199 | # The font size ('10pt', '11pt' or '12pt'). 200 | #'pointsize': '10pt', 201 | 202 | # Additional stuff for the LaTeX preamble. 203 | #'preamble': '', 204 | } 205 | 206 | # Grouping the document tree into LaTeX files. List of tuples 207 | # (source start file, target name, title, 208 | # author, documentclass [howto, manual, or own class]). 209 | latex_documents = [ 210 | ('index', 'python-devicecloud.tex', u'python-devicecloud Documentation', 211 | u'Paul Osborne, Tom Manley, Stephen Stack, Brandon Moser', 'manual'), 212 | ] 213 | 214 | # The name of an image file (relative to this directory) to place at the top of 215 | # the title page. 216 | #latex_logo = None 217 | 218 | # For "manual" documents, if this is true, then toplevel headings are parts, 219 | # not chapters. 220 | #latex_use_parts = False 221 | 222 | # If true, show page references after internal links. 223 | #latex_show_pagerefs = False 224 | 225 | # If true, show URL addresses after external links. 226 | #latex_show_urls = False 227 | 228 | # Documents to append as an appendix to all manuals. 229 | #latex_appendices = [] 230 | 231 | # If false, no module index is generated. 232 | #latex_domain_indices = True 233 | 234 | 235 | # -- Options for manual page output --------------------------------------- 236 | 237 | # One entry per manual page. List of tuples 238 | # (source start file, name, description, authors, manual section). 239 | man_pages = [ 240 | ('index', 'python-devicecloud', u'python-devicecloud Documentation', 241 | [u'Paul Osborne, Tom Manley, Stephen Stack'], 1) 242 | ] 243 | 244 | # If true, show URL addresses after external links. 245 | #man_show_urls = False 246 | 247 | 248 | # -- Options for Texinfo output ------------------------------------------- 249 | 250 | # Grouping the document tree into Texinfo files. List of tuples 251 | # (source start file, target name, title, author, 252 | # dir menu entry, description, category) 253 | texinfo_documents = [ 254 | ('index', 'python-devicecloud', u'python-devicecloud Documentation', 255 | u'Paul Osborne, Tom Manley, Stephen Stack', 'python-devicecloud', 256 | 'Device Cloud Web Services Client for Python', 257 | 'Miscellaneous'), 258 | ] 259 | 260 | # Documents to append as an appendix to all manuals. 261 | #texinfo_appendices = [] 262 | 263 | # If false, no module index is generated. 264 | #texinfo_domain_indices = True 265 | 266 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 267 | #texinfo_show_urls = 'footnote' 268 | 269 | # If true, do not generate a @detailmenu in the "Top" node's menu. 270 | #texinfo_no_detailmenu = False 271 | 272 | autodoc_member_order = 'bysource' # order in docs same as in source code 273 | -------------------------------------------------------------------------------- /docs/cookbook.rst: -------------------------------------------------------------------------------- 1 | Cookbook 2 | ========= 3 | 4 | This page is meant to show complete examples for common uses of the library. 5 | For more granular or specific examples of API usage check out the individual API pages. 6 | 7 | .. note:: 8 | 9 | There are also examples checked into source control under /devicecloud/examples/\*_playground.py 10 | which will provide additional example uses of the library. 11 | 12 | Each example will assume an instance of :class:`devicecloud.DeviceCloud` has been 13 | created with something like so:: 14 | 15 | dc = DeviceCloud(, ) 16 | 17 | Streams - Creating Streams and Data Points 18 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 19 | 20 | For this example, let's create a DataStream that represents a class room. As students 21 | enter the classroom there is a DataPoint representing them written to the stream. 22 | 23 | First let's get or create the stream:: 24 | 25 | classroom = dc.streams.get_stream_if_exists('classroom') 26 | if not classroom: 27 | classroom = dc.streams.create_stream( 28 | stream_id='classroom', 29 | data_type=STREAM_TYPE_STRING, 30 | description='Stream representing a classroom of students', 31 | ) 32 | 33 | A student named Bob enters the classroom so we write a DataPoint representing Bob 34 | to the stream. To represent Bob let's use a JSON object:: 35 | 36 | student = { 37 | 'name': 'Bob', 38 | 'student_id': 12, 39 | 'age': 21, 40 | } 41 | datapoint = DataPoint(data=json.dumps(student)) 42 | classroom.write(datapoint) 43 | 44 | Now, two students enter at the same time. For this we can write both data points 45 | as a batch which only uses one HTTP request (up to 250 data points):: 46 | 47 | students = [ 48 | { 49 | 'name': 'James', 50 | 'student_id': 13, 51 | 'age': 22, 52 | }, 53 | { 54 | 'name': 'Henry', 55 | 'student_id': 14, 56 | 'age': 20, 57 | } 58 | ] 59 | datapoints = [DataPoint(data=json.dumps(x)) for x in students] 60 | classroom.bulk_write_datapoints(datapoints) 61 | 62 | Finally, let's print the name of the student who most recently entered the classroom:: 63 | 64 | most_recent_student = classroom.get_current_value() 65 | print json.loads(most_recent_student.get_data())['name'] # Prints 'Henry' 66 | 67 | 68 | Streams - Deleting 69 | ^^^^^^^^^^^^^^^^^^^^ 70 | 71 | Let's delete the classroom stream from the above example so we can start fresh in the 72 | next example:: 73 | 74 | classroom.delete() 75 | 76 | Streams - Roll-up Data 77 | ^^^^^^^^^^^^^^^^^^^^^^^^ 78 | 79 | Roll-up data is a way to group data points based on time intervals in which they 80 | were written to the cloud. From our previous example lets figure out which students 81 | entered the classroom throughout the day and which hour they entered. 82 | 83 | First let's write some test data to the cloud. Since roll-ups only work on numerical 84 | data types we will student id's instead of JSON. 85 | 86 | Create a new data stream that is of type int:: 87 | 88 | classroom = dc.streams.create_stream( 89 | stream_id='classroom', 90 | data_type=STREAM_TYPE_INTEGER, 91 | description='Stream representing a classroom of students (as id's)', 92 | ) 93 | 94 | Next is a function that fills the classroom with data points that have randomly 95 | generated timestamp values within the next 24 hours:: 96 | 97 | now = time.time() 98 | one_day_in_seconds = 86400 99 | 100 | datapoints = list() 101 | for student_id in xrange(100): 102 | deviation = random.randint(0, one_day_in_seconds) 103 | random_time = now + deviation 104 | datapoint = DataPoint(data=student_id, 105 | timestamp=datetime.datetime.fromtimestamp(random_time)) 106 | datapoints.append(datapoint) 107 | 108 | classroom.bulk_write_datapoints(datapoints) 109 | 110 | Finally, let's figure out which students entered the classroom which hours of the day:: 111 | 112 | rollup_data = classroom.read(rollup_interval='hour', rollup_method='count') 113 | hourly_data = {} 114 | for dp in rollup_data: 115 | hourly_data[dp.get_timestamp().hour] = dp.get_data() 116 | pprint.pprint(hourly_data) 117 | 118 | The result is a dictionary where the key's are the hour in the day and the values are the 119 | number of students who entered the classroom that hour:: 120 | 121 | {0: 10, 122 | 1: 10, 123 | 2: 9, 124 | 3: 3, 125 | 4: 3, 126 | 5: 6, 127 | 6: 9, 128 | 7: 11, 129 | 8: 5, 130 | 9: 7, 131 | 10: 9, 132 | 11: 9, 133 | 12: 7, 134 | 13: 6, 135 | 14: 13, 136 | 15: 8, 137 | 16: 13, 138 | 17: 9, 139 | 18: 7, 140 | 19: 7, 141 | 20: 11, 142 | 21: 8, 143 | 22: 6, 144 | 23: 11} 145 | 146 | 147 | Device Core - Groups 148 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 149 | 150 | .. note:: 151 | 152 | This assumes your device is provisioned. 153 | 154 | First, get a reference to the device which you would like to add a specific group:: 155 | 156 | device = devicecore.get_device('00:40:9D:50:B0:EA') 157 | 158 | Then you can add it to a group and fetch it to make sure it works:: 159 | 160 | device.add_to_group('mygroup') 161 | device.get_group_path() # prints 'mygroup' (DC sometimes needs a second to catch up) 162 | 163 | Or remove it:: 164 | 165 | device.remove_from_group() 166 | 167 | 168 | Device Core - Tags 169 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 170 | 171 | .. note:: 172 | 173 | This assumes your device is provisioned. 174 | 175 | Similar to Groups, get a reference to the device which you would like to add a specific group:: 176 | 177 | device = devicecore.get_device('00:40:9D:50:B0:EA') 178 | 179 | Then you can add a tag and then get the new list:: 180 | 181 | device.add_tags('mytag') 182 | device.get_tags() # prints ['mytags'] (DC sometimes needs a second to catch up) 183 | 184 | Or remove it:: 185 | 186 | device.remove_tag('mytag') 187 | device.get_tags() # prints [] (DC sometimes needs a second to catch up) 188 | -------------------------------------------------------------------------------- /docs/core.rst: -------------------------------------------------------------------------------- 1 | Core API 2 | ======== 3 | 4 | DeviceCloud Core API 5 | -------------------- 6 | 7 | The :class:`devicecloud.DeviceCloud` class contains the core interface which 8 | will be used by all clients using the devicecloud library. 9 | 10 | .. automodule:: devicecloud 11 | :members: 12 | 13 | Conditions API 14 | -------------- 15 | 16 | .. automodule:: devicecloud.conditions 17 | :members: 18 | -------------------------------------------------------------------------------- /docs/devicecore.rst: -------------------------------------------------------------------------------- 1 | DeviceCore API 2 | ============== 3 | 4 | DeviceCore Overview 5 | ------------------- 6 | 7 | DeviceCore provides access to core device information such as which 8 | devices are in a given device cloud account, which of those are 9 | connected, etc. 10 | 11 | DeviceCore API Documentation 12 | ---------------------------- 13 | 14 | .. automodule:: devicecloud.devicecore 15 | :members: 16 | -------------------------------------------------------------------------------- /docs/filedata.rst: -------------------------------------------------------------------------------- 1 | FileData API 2 | ============ 3 | 4 | FileData Overview 5 | ----------------- 6 | 7 | The FileData store on Device Cloud provides a hierarchical mechanism for temporarily 8 | storing information in files sent from devices. With the APIs provided by the device 9 | cloud, it is possible to use the FileData store in a number of different ways to implement 10 | various use cases. 11 | 12 | There are two main ways of thinking about the FileData store: 13 | 14 | 1. As hierarchical data store for temporarily storing data pushes from devices 15 | 2. As a message queue 16 | 17 | The usage for both scenarios is similar. In the first case, it is likely that a web 18 | service will poll the FileData store for new files matching some criterion on a periodic 19 | basis. If using the FileData store as a queue, one will likely want to setup monitors 20 | on FileData paths matching certain criterion. The set of files matching some condition 21 | can then be thought of as a channel. 22 | 23 | This library seeks to make using Device Cloud for both of these use cases simple 24 | and robust. 25 | 26 | Navigating the FileData Store 27 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 28 | 29 | There are a few main ways to navigate the FileData store. 30 | 31 | Method 1: Iterate through the file tree by getting the filedata 32 | objects associated with paths or other conditions:: 33 | 34 | condtion = (fd_path == '~/') 35 | for filedata in dc.filedata.get_filedata(condition): 36 | print filedata 37 | 38 | The :meth:`.FileDataAPI.get_filedata` method will return a generator 39 | over the set of FileData directories and files matching the provided 40 | conditions. The two types availble (which are both instances of 41 | :class:`.FileDataObject` are :class:`.FileDataDirectory` and 42 | :class:`.FileDataFile`. 43 | 44 | Other methods of navigating the store are built upon the functionality 45 | provided by :meth:`.FileDataAPI.get_filedata`. 46 | 47 | Method 2: The :meth:`.FileDataAPI.walk` method provides a convenient 48 | way to iterate over all files and directories starting with some root 49 | within the filedata store. This method mimicks the interface and 50 | behavior provided by the python standard library :meth:`os.walk` 51 | which is used to iterate over one's local filesystem. 52 | 53 | Here is a basic example:: 54 | 55 | for dirname, directories, files in dc.filedata.walk(): 56 | for file in files: 57 | print file.get_full_path() 58 | 59 | It is also possible to perform a walk from a given :class:`.FileDataDirectory` 60 | as follows:: 61 | 62 | d = dc.filedata.get_filedata("~/mydevice/mydir") 63 | for dirname, directories, files in d.walk(): 64 | for file in files: 65 | print file.get_full_path() 66 | 67 | 68 | Reading Files and File Metadata 69 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 70 | 71 | After finding a :class:`.FileDataFile` using either :meth:`.FileDataAPI.get_filedata` 72 | or :meth:`.FileDataAPI.walk`, access to file contents and metadata is provided 73 | by a set of straightforward accessors. Many of these are also available on 74 | :class:`.FileDataDirectory` objects as well. Here we show the basic methods:: 75 | 76 | # f is a FileDataFile object 77 | f.get_data() # the data contained in the file 78 | f.get_last_modified_date() # returns datetime object 79 | f.get_type() # returns 'file'. 'directory' for directories 80 | f.get_content_type() # returns content type (e.g. 'application/xml') 81 | f.get_customer_id() # returns customer id (string) 82 | f.get_created_date() # returns datetime object 83 | f.get_name() # returns name of file (e.g. test.txt) 84 | f.get_path() # returns path leading up to the file (e.g. /a/b/c) 85 | f.get_full_path() # returns full path including name (e.g. /a/b/c/test.txt) 86 | f.get_size() # returns the size of the file in bytes as int 87 | 88 | 89 | Creating a Directory 90 | ~~~~~~~~~~~~~~~~~~~~ 91 | 92 | TODO: This functionality is not current active. 93 | 94 | There are three ways to create a new directory: 95 | 96 | 1. Create full path with :meth:`.FileDataAPI.make_dirs`. This will recursively 97 | create the full path specified. 98 | 2. Write a file to a directory that does not yet exist. If the path is valid, 99 | all directories should be created recursively. 100 | 3. By calling :meth:`.FileDataDirectory.add_subdirectory` 101 | 102 | Different methods may suit your needs depending on your use cases. 103 | 104 | Writing a File 105 | ~~~~~~~~~~~~~~ 106 | 107 | The following methods may be used to write a file: 108 | 109 | 1. Use :meth:`.FileDataAPI.write_file`. This requires a full path to be specified. 110 | 2. Use :meth:`.FileDataDirectory.write_file`. As you already have a directory path 111 | in that case, you do not need to specify the path leading to the file. 112 | 113 | Here's a basic example:: 114 | 115 | dc.filedata.write_file( 116 | path="~/test", 117 | name="test.json", 118 | content_type="application/json", 119 | archive=False 120 | ) 121 | 122 | Viewing File History 123 | ~~~~~~~~~~~~~~~~~~~~ 124 | 125 | .. note:: Support for programmatically getting history for files is not 126 | supported at this time. 127 | 128 | API Documentation 129 | ----------------- 130 | 131 | The filedata module provides function for reading, writing, and 132 | deleting "files" from Device Cloud FileData store. 133 | 134 | .. automodule:: devicecloud.filedata 135 | :members: 136 | -------------------------------------------------------------------------------- /docs/filesystem.rst: -------------------------------------------------------------------------------- 1 | File System Service API 2 | ================================== 3 | 4 | File System Service Overview 5 | ---------------------------- 6 | 7 | Provide access to Device Cloud File System commands that use SCI to 8 | get the data from your devices connected to the cloud. 9 | 10 | File System Service API Documentation 11 | ------------------------------------- 12 | 13 | .. automodule:: devicecloud.file_system_service 14 | :members: 15 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | python-devicecloud 2 | ****************** 3 | 4 | Documention Map 5 | =============== 6 | 7 | .. toctree:: 8 | :maxdepth: 1 9 | 10 | core 11 | devicecore 12 | streams 13 | filedata 14 | sci 15 | filesystem 16 | monitor 17 | ws 18 | cookbook 19 | 20 | Introduction 21 | ============ 22 | 23 | Python-devicecloud is a library providing simple, intuitive access to 24 | the `Digi Device Cloud 25 | `_ for clients written 26 | in Python. 27 | 28 | The library wraps Device Cloud REST API and hides the details of 29 | forming HTTP requests in order to gain access to device information, 30 | file data, streams, and other features of Device Cloud. The API 31 | wrapped can be found `here 32 | `_. 33 | 34 | 35 | The primary target audience for this library is individuals 36 | interfacing with Device Cloud from the server side or developers 37 | writing tools to aid device development. For efficient connectivity 38 | from devices, we suggest that you first look at using the `Device 39 | Cloud Connector `_. 40 | That being said, this library could also be used on devices if deemed 41 | suitable. 42 | 43 | The library provides access to a wide array of features, but here is a 44 | quick example of what the API looks like:: 45 | 46 | from devicecloud import DeviceCloud 47 | 48 | dc = DeviceCloud('user', 'pass') 49 | 50 | # show the MAC address of all devices that are currently connected 51 | # 52 | # This is done using Device Cloud DeviceCore functionality 53 | print "== Connected Devices ==" 54 | for device in dc.devicecore.get_devices(): 55 | if device.is_connected(): 56 | print device.get_mac() 57 | 58 | # get the name and current value of all data streams having values 59 | # with a floating point type 60 | # 61 | # This is done using Device Cloud stream functionality 62 | for stream in dc.streams.get_streams(): 63 | if stream.get_data_type().lower() in ('float', 'double'): 64 | print "%s -> %s" % (stream.get_stream_id(), stream.get_current_value()) 65 | 66 | Indices and tables 67 | ================== 68 | 69 | * :ref:`genindex` 70 | * :ref:`modindex` 71 | * :ref:`search` 72 | 73 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\python-devicecloud.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\python-devicecloud.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /docs/monitor.rst: -------------------------------------------------------------------------------- 1 | Monitor API 2 | =========== 3 | 4 | Monitor Overview 5 | ---------------- 6 | 7 | Provide access to Device Cloud monitor API which can be used to 8 | subscribe to topics to receive notifications when data is received 9 | on Device Cloud. 10 | 11 | SCI API Documentation 12 | --------------------- 13 | 14 | .. automodule:: devicecloud.monitor 15 | :members: 16 | 17 | .. automodule:: devicecloud.monitor_tcp 18 | :members: 19 | -------------------------------------------------------------------------------- /docs/sci.rst: -------------------------------------------------------------------------------- 1 | SCI (Server Command Interface) API 2 | ================================== 3 | 4 | SCI Overview 5 | ------------ 6 | 7 | Provide access to Device Cloud Server Command Interface used for 8 | sending messages to devices connected to Device Cloud. 9 | 10 | SCI API Documentation 11 | --------------------- 12 | 13 | .. automodule:: devicecloud.sci 14 | :members: 15 | -------------------------------------------------------------------------------- /docs/streams.rst: -------------------------------------------------------------------------------- 1 | Streams API 2 | =========== 3 | 4 | Streams Overview 5 | ---------------- 6 | 7 | Data Streams on Device Cloud provide a mechanism for storing time-series 8 | values over a long period of time. Each individual value in the time series 9 | is known as a Data Point. 10 | 11 | There are a few basic operations supported by Device Cloud on streams which 12 | are supported by Device Cloud and this library. Here we give examples of 13 | each. 14 | 15 | Listing Streams 16 | ^^^^^^^^^^^^^^^ 17 | 18 | Although it is not recommended for production applications, it is often useful 19 | when building tools to be able to fetch a list of all streams. This can be 20 | done by using :meth:`.StreamsAPI.get_streams`:: 21 | 22 | dc = DeviceCloud('user', 'pass') 23 | for stream in dc.streams.get_streams(): 24 | print "%s: %s" % (stream.get_stream_id(), 25 | stream.get_description()) 26 | 27 | Creating a Stream 28 | ^^^^^^^^^^^^^^^^^ 29 | 30 | Streams can be created in two ways, both of which are supported by this library. 31 | 32 | 1. Create a stream explicitly using :meth:`.StreamsAPI.create_stream` 33 | 2. Get a reference to a stream using :meth:`.StreamsAPI.get_stream` 34 | that does not yet exist and write a datapoint to it. 35 | 36 | Here's examples of these two methods for creating a new stream:: 37 | 38 | dc = DeviceCloud('user', 'pass') 39 | 40 | # explicitly create a new data stream 41 | humidity_stream = dc.streams.create_stream( 42 | stream_id="mystreams/hudidity", 43 | data_type="float", 44 | description="Humidity") 45 | humidity_stream.write(Datapoint(81.2)) 46 | 47 | # create data stream implicitly 48 | temperature_stream = streams.get_stream("/%s/temperature" % some_id) 49 | temperature_stream.write(Datapoint( 50 | stream_id="mystreams/temperature" % some_id, 51 | data=74.1, 52 | description="Outside Air Temperature in F", 53 | data_type=STREAM_TYPE_FLOAT, 54 | unit="Degrees Fahrenheit" 55 | )) 56 | 57 | Getting Information About A Stream 58 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 59 | 60 | Whether we know a stream by id and have gotten a reference using 61 | :meth:`.StreamsAPI.get_stream` or have discovered it using 62 | :meth:`.StreamsAPI.get_streams`, the :class:`.DataStream` should 63 | be able to provide access to all metadata about the stream that 64 | you may need. Here we show several of them:: 65 | 66 | strm = dc.streams.get_stream("test") 67 | print strm.get_stream_id() 68 | print strm.get_data_type() 69 | print strm.get_units() 70 | print strm.get_description() 71 | print strm.get_data_ttl() 72 | print strm.get_rollup_ttl() 73 | print strm.get_current_value() # return DataPoint object 74 | 75 | .. note:: 76 | 77 | :meth:`.DataStream.get_current_value()` does not use cached values by 78 | default and will make a web service call to get the most recent current 79 | value unless ``use_cached`` is set to True when called. 80 | 81 | Deleting a Stream 82 | ^^^^^^^^^^^^^^^^^ 83 | 84 | Deleting a data stream is possible by calling :meth:`.DataStream.delete`:: 85 | 86 | strm = dc.streams.get_stream("doomed") 87 | strm.delete() 88 | 89 | Updating Stream Metadata 90 | ^^^^^^^^^^^^^^^^^^^^^^^^ 91 | 92 | This feature is currently not supported. Some stream information may 93 | be updated by writing a :class:`.DataPoint` and including updated 94 | stream info elements. 95 | 96 | DataPoint objects 97 | ^^^^^^^^^^^^^^^^^ 98 | 99 | The :class:`.DataPoint` class encapsulates all information required for 100 | both writing data points as well as retrieving information about data 101 | points stored on Device Cloud. 102 | 103 | API Documentation 104 | ----------------- 105 | 106 | .. automodule:: devicecloud.streams 107 | :members: 108 | -------------------------------------------------------------------------------- /docs/ws.rst: -------------------------------------------------------------------------------- 1 | Direct Web Services API 2 | ======================= 3 | 4 | Device Cloud exposes a large set of functionality to users and the 5 | python-devicecloud library seeks to provide convenient and complete 6 | APIs for a majority of these. However, there are APIs which the library 7 | does not cover; some may have coverage in the future and others may never 8 | have direct support in the library. 9 | 10 | The "ws" API provides a mechanism for directly making calls to 11 | unsupported web services APIs. An example of the syntax exposed by the library 12 | is probably best demonstrated with an example. 13 | 14 | An example of an API not currently supported by the library is the `Alarms API 15 | `_. 16 | This API is a "legacy" API (it is not prefixed with a "v1") and its basic interface is GET, POST, PUT, and 17 | DELETE of the path "/ws/Alarm". The API returns response and expects payloads to be in XML according 18 | to a format described in the documentation:: 19 | 20 | # List alarms 21 | # 22 | # Retrieves results of GET to /ws/Alarms with authentication and will raise 23 | # the standard exceptions in the case of a failure response. 24 | # 25 | >>> dc = devicecloud.DeviceCloud('user', 'pass') 26 | >>> response = dc.ws.Alarm.get() 27 | >>> print response.content 28 | ... A bunch of XML ... 29 | >>> print dc.ws.Alarm.get_json() 30 | ... A bunch of JSON ... 31 | >>> print list(dc.ws.Alarm.iter_json_pages()) 32 | ... All Alarms over all pages with result as list of dictionaries ... 33 | 34 | # 35 | # Note that in the syntactic sugar may fall short. In those cases, you 36 | # may need to fall back to using the underlying DeviceConnection 37 | # 38 | >>> alarm_id = 10 39 | >>> print dc.get_connection().get_json("/ws/Alarm/{}".format(alarm_id)) 40 | ... Some JSON ... 41 | 42 | For more details, refer to the documentation on the methods for :py:class:`devicecloud.DeviceCloudConnection`. 43 | -------------------------------------------------------------------------------- /inttest.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # This script will run all unit and integration 4 | # tests. In order to run the integration tests, we 5 | # need to get a username and password, which is what 6 | # this script does. The rest of the work is done 7 | # by the 'toxtest.sh' script 8 | # 9 | 10 | export RUN_INTEGRATION_TESTS=yes 11 | if [ -z $DC_USERNAME ]; then 12 | echo -n 'username: ' 13 | read username 14 | export DC_USERNAME="$username" 15 | fi 16 | 17 | if [ -z $DC_PASSWORD ]; then 18 | echo -n 'password: ' 19 | read -s password 20 | export DC_PASSWORD="$password" 21 | fi 22 | 23 | ./toxtest.sh 24 | -------------------------------------------------------------------------------- /make.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | rem Install/Upgrade Build Tools and Dependencies 4 | pip install --upgrade setuptools wheel 5 | pip install --upgrade twine 6 | 7 | rem Build distribution 8 | python setup.py sdist bdist_wheel 9 | 10 | rem Upload to public Pypi Server 11 | python -m twine upload dist/* 12 | 13 | rem Upload to Internal Pypi Server 14 | python setup.py sdist upload -i http://pypi.digi.com/simple 15 | 16 | rem "Package uploaded to Pypi Servers" 17 | -------------------------------------------------------------------------------- /make.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Install/Upgrade Build Tools and Dependencies 4 | python -m pip install --upgrade setuptools wheel 5 | python -m pip install --upgrade twine 6 | 7 | # Build distribution 8 | python setup.py sdist bdist_wheel 9 | 10 | # Upload to public Pypi Server 11 | python -m twine upload dist/* 12 | 13 | # Upload to Internal Pypi Server 14 | python setup.py sdist upload -i http://pypi.digi.com/simple 15 | 16 | echo "Package uploaded to Pypi Servers" 17 | 18 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | arrow>=1.2.2 2 | backports.functools-lru-cache>=1.5 3 | certifi>=2021.10.8 4 | chardet>=4.0.0 5 | idna>=3.3 6 | python-dateutil>=2.8.2 7 | requests>=2.27.1 8 | six>=1.16.0 9 | urllib3>=1.22 10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | # 5 | # Copyright (c) 2015-2018 Digi International Inc. 6 | 7 | import re 8 | import os 9 | from setuptools import setup, find_packages 10 | 11 | VERSIONFILE = "devicecloud/version.py" 12 | 13 | def get_version(): 14 | # In order to get the version safely, we read the version.py file 15 | # as text. This is necessary as devicecloud/__init__.py uses 16 | # things that won't yet be present when the package is being 17 | # installed. 18 | verstrline = open(VERSIONFILE, "r").read() 19 | version_string_re = re.compile(r"^__version__ = ['\"]([^'\"]*)['\"]", re.MULTILINE) 20 | match = version_string_re.search(verstrline) 21 | if match: 22 | return match.group(1) 23 | else: 24 | raise RuntimeError("Unable to find version string in %s." % (VERSIONFILE,)) 25 | 26 | 27 | def get_long_description(): 28 | long_description = open('README.md').read() 29 | try: 30 | import subprocess 31 | import pandoc 32 | doc = pandoc.Document() 33 | doc.markdown = long_description.encode('utf-8') 34 | open("README.rst", "wb").write(doc.rst) 35 | except: 36 | print("Could not find pandoc or convert properly") 37 | print(" make sure you have pandoc (system) and pyandoc (python module) installed") 38 | 39 | return long_description 40 | 41 | setup( 42 | name="devicecloud", 43 | version=get_version(), 44 | description="Python API to the Digi Device Cloud", 45 | long_description_content_type='text/markdown', 46 | long_description=get_long_description(), 47 | url="https://github.com/digidotcom/python-devicecloud", 48 | author="Digi International Inc.", 49 | author_email="brandon.moser@digi.com", 50 | packages=find_packages(), 51 | install_requires=open('requirements.txt').read().split(), 52 | classifiers=[ 53 | "Development Status :: 4 - Beta", 54 | "Intended Audience :: Developers", 55 | "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)", 56 | "Programming Language :: Python :: 2.7", 57 | "Programming Language :: Python :: 3", 58 | "Programming Language :: Python :: 3.4", 59 | "Programming Language :: Python :: 3.5", 60 | "Programming Language :: Python :: 3.6", 61 | "Programming Language :: Python :: 3.7", 62 | "Topic :: Software Development :: Libraries", 63 | "Operating System :: OS Independent", 64 | ], 65 | ) 66 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | mock 3 | nose 4 | httpretty 5 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py34,py35,py36,py37,pypy,pypy3 3 | 4 | [testenv] 5 | passenv = * 6 | deps = 7 | commands = 8 | pip install -r test-requirements.txt 9 | nosetests -m '^(int|unit)?[Tt]est' 10 | 11 | [testenv:coverage] 12 | deps= 13 | {[testenv]deps} 14 | coverage==3.7.1 15 | python-coveralls 16 | commands = 17 | coverage run --branch --omit={envdir}/* {envbindir}/nosetests 18 | coveralls 19 | -------------------------------------------------------------------------------- /toxtest.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Script that will try to test this codebase against as many 4 | # python versions as is possible. It does this using a combination 5 | # of pyenv (for building various interpreters) and tox for 6 | # testing using each of those interpreters. 7 | # 8 | 9 | pyversions=(2.7.16 10 | 3.4.10 11 | 3.5.7 12 | 3.6.8 13 | 3.7.3 14 | pypy2.7-6.0.0 15 | pypy3.5-6.0.0) 16 | 17 | # first make sure that pyenv is installed 18 | if [ ! -s "$HOME/.pyenv/bin/pyenv" ]; then 19 | curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash 20 | fi 21 | 22 | # Update pyenv (required for new python versions to be available) 23 | pyenv update 24 | 25 | # add pyenv to our path and initialize (if this has not already been done) 26 | export PATH="$HOME/.pyenv/bin:$PATH" 27 | eval "$(pyenv init -)" 28 | 29 | # install each python version that we want to test with 30 | for pyversion in ${pyversions[*]}; 31 | do 32 | pyenv install -s ${pyversion} 33 | done 34 | pyenv rehash 35 | 36 | # This is required 37 | pyenv global ${pyversions[*]} 38 | 39 | # Now, run the tests after sourcing venv for tox install/use 40 | virtualenv -q .toxenv 41 | source .toxenv/bin/activate 42 | pip install -q -r test-requirements.txt 43 | tox --recreate 44 | --------------------------------------------------------------------------------