├── .gitignore ├── .gitreview ├── CHANGELOG ├── HOW_TO_RELEASE ├── LICENSE ├── Pipfile ├── Pipfile.lock ├── README.md ├── package.bat ├── run_tests.py ├── samples └── interactive_client.py ├── setup.cfg ├── setup.py ├── setup_pyenv.py ├── tox.ini └── waapi ├── __init__.py ├── client ├── __init__.py ├── client.py ├── event.py ├── executor.py └── interface.py ├── test ├── __init__.py ├── fixture.py ├── test_allowed_exception.py ├── test_connection.py ├── test_executor.py ├── test_integration.py ├── test_large_payload.py ├── test_rpc_lowlevel.py └── test_subscribe_lowlevel.py └── wamp ├── __init__.py ├── ak_autobahn.py ├── async_compatibility.py ├── async_decoupled_client.py └── interface.py /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/macos,python,pycharm 3 | 4 | .vscode 5 | docs 6 | 7 | ### macOS ### 8 | # General 9 | .DS_Store 10 | .AppleDouble 11 | .LSOverride 12 | 13 | # Icon must end with two \r 14 | Icon 15 | 16 | # Thumbnails 17 | ._* 18 | 19 | # Files that might appear in the root of a volume 20 | .DocumentRevisions-V100 21 | .fseventsd 22 | .Spotlight-V100 23 | .TemporaryItems 24 | .Trashes 25 | .VolumeIcon.icns 26 | .com.apple.timemachine.donotpresent 27 | 28 | # Directories potentially created on remote AFP share 29 | .AppleDB 30 | .AppleDesktop 31 | Network Trash Folder 32 | Temporary Items 33 | .apdisk 34 | 35 | ### PyCharm ### 36 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 37 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 38 | 39 | # User-specific stuff 40 | .idea/**/workspace.xml 41 | .idea/**/tasks.xml 42 | .idea/**/usage.statistics.xml 43 | .idea/**/dictionaries 44 | .idea/**/shelf 45 | 46 | # Sensitive or high-churn files 47 | .idea/**/dataSources/ 48 | .idea/**/dataSources.ids 49 | .idea/**/dataSources.local.xml 50 | .idea/**/sqlDataSources.xml 51 | .idea/**/dynamic.xml 52 | .idea/**/uiDesigner.xml 53 | .idea/**/dbnavigator.xml 54 | 55 | # Gradle 56 | .idea/**/gradle.xml 57 | .idea/**/libraries 58 | 59 | # Gradle and Maven with auto-import 60 | # When using Gradle or Maven with auto-import, you should exclude module files, 61 | # since they will be recreated, and may cause churn. Uncomment if using 62 | # auto-import. 63 | # .idea/modules.xml 64 | # .idea/*.iml 65 | # .idea/modules 66 | 67 | # CMake 68 | cmake-build-*/ 69 | 70 | # Mongo Explorer plugin 71 | .idea/**/mongoSettings.xml 72 | 73 | # File-based project format 74 | *.iws 75 | 76 | # IntelliJ 77 | out/ 78 | 79 | # mpeltonen/sbt-idea plugin 80 | .idea_modules/ 81 | 82 | # JIRA plugin 83 | atlassian-ide-plugin.xml 84 | 85 | # Cursive Clojure plugin 86 | .idea/replstate.xml 87 | 88 | # Crashlytics plugin (for Android Studio and IntelliJ) 89 | com_crashlytics_export_strings.xml 90 | crashlytics.properties 91 | crashlytics-build.properties 92 | fabric.properties 93 | 94 | # Editor-based Rest Client 95 | .idea/httpRequests 96 | 97 | ### PyCharm Patch ### 98 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 99 | 100 | # *.iml 101 | # modules.xml 102 | # .idea/misc.xml 103 | # *.ipr 104 | 105 | # Sonarlint plugin 106 | .idea/sonarlint 107 | 108 | ### Python ### 109 | # Byte-compiled / optimized / DLL files 110 | __pycache__/ 111 | *.py[cod] 112 | *$py.class 113 | 114 | # C extensions 115 | *.so 116 | 117 | # Distribution / packaging 118 | .Python 119 | build/ 120 | develop-eggs/ 121 | dist/ 122 | downloads/ 123 | eggs/ 124 | .eggs/ 125 | lib/ 126 | lib64/ 127 | parts/ 128 | sdist/ 129 | var/ 130 | wheels/ 131 | *.egg-info/ 132 | .installed.cfg 133 | *.egg 134 | MANIFEST 135 | 136 | # PyInstaller 137 | # Usually these files are written by a python script from a template 138 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 139 | *.manifest 140 | *.spec 141 | 142 | # Installer logs 143 | pip-log.txt 144 | pip-delete-this-directory.txt 145 | 146 | # Unit test / coverage reports 147 | htmlcov/ 148 | .tox/ 149 | .coverage 150 | .coverage.* 151 | .cache 152 | nosetests.xml 153 | coverage.xml 154 | *.cover 155 | .hypothesis/ 156 | .pytest_cache/ 157 | 158 | # Translations 159 | *.mo 160 | *.pot 161 | 162 | # Django stuff: 163 | *.log 164 | local_settings.py 165 | db.sqlite3 166 | 167 | # Flask stuff: 168 | instance/ 169 | .webassets-cache 170 | 171 | # Scrapy stuff: 172 | .scrapy 173 | 174 | # Sphinx documentation 175 | docs/_build/ 176 | 177 | # PyBuilder 178 | target/ 179 | 180 | # Jupyter Notebook 181 | .ipynb_checkpoints 182 | 183 | # pyenv 184 | .python-version 185 | 186 | # celery beat schedule file 187 | celerybeat-schedule 188 | 189 | # SageMath parsed files 190 | *.sage.py 191 | 192 | # Environments 193 | .env 194 | .venv 195 | env/ 196 | venv/ 197 | ENV/ 198 | env.bak/ 199 | venv.bak/ 200 | 201 | # Spyder project settings 202 | .spyderproject 203 | .spyproject 204 | 205 | # Rope project settings 206 | .ropeproject 207 | 208 | # mkdocs documentation 209 | /site 210 | 211 | # mypy 212 | .mypy_cache/ 213 | 214 | ### Python Patch ### 215 | .venv/ 216 | 217 | 218 | # End of https://www.gitignore.io/api/macos,python,pycharm 219 | -------------------------------------------------------------------------------- /.gitreview: -------------------------------------------------------------------------------- 1 | [gerrit] 2 | host=git.audiokinetic.inte 3 | project=waapi-client-python 4 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | # Version 0.7.2 2 | ## Misc 3 | * Update Python version support window 4 | 5 | # Version 0.7.1 6 | ## Misc 7 | * Fix readme sample 8 | * Update Pipfile.lock 9 | 10 | # Version 0.7 11 | ## Bugfixes 12 | * WG-65726 Unreal Hang due to Waapi Call through Python (UE5.1) 13 | 14 | ## Misc 15 | * Added `Python 3.11` to the tested runtimes 16 | * Added `run-tests.py` to make it easier to run the test suite 17 | 18 | ## Behavior 19 | With WG-65726, `enable_debug_log` now assigns the `DEBUG` logging level to the global logging config. 20 | 21 | # Version 0.6 22 | ## Bugfixes 23 | * WG-54691 Message payloads size is exceeded when using ak.wwise.core.audio.import with 100+ files 24 | * WG-54781 Users can't guaranty order of publish events 25 | 26 | ## Behavior 27 | With WG-54781, a new argument was added to WaapiClient's constructor to specify an execution strategy for event 28 | handler callbacks. Three strategies are bundled with the `waapi` module, with `PerCallbackThreadExecutor` acting 29 | identically to version 0.5 and `SequentialThreadExecutor` acting as the new default. 30 | 31 | * SequentialThreadExecutor (default): A single thread processes callbacks sequential, in order of reception 32 | * PerCallbackThreadExecutor (old default): A new thread runs each callback, in the order they are scheduled 33 | * AsyncioLoopExecutor: Processes the callback on the main asyncio loop (does not support calls to WaapiClient instances) 34 | 35 | It is also easy for users to specify a custom strategy by implementing the `CallbackExecutor` interface. 36 | 37 | ## Misc 38 | * WG-54779 Update pipfile.lock (Closes #7, Closes #9) 39 | 40 | # Version 0.5 41 | ## Bugfixes 42 | * WG-51774 Cannot use ak.wwise.waapi.getSchema because the uri keyword is used in WaapiClient.call (Closes #5) 43 | * WG-51607 Migrate from coroutine to def async (Closes #4) 44 | 45 | ## Misc 46 | * Updated Python requirements and moved to tox test frontend 47 | 48 | # Version 0.4 49 | ## Bugfixes 50 | * WG-47527 Multiple calls to disconnect cause hang 51 | * WG-44991 Add support for subprocesses (Closes #1) 52 | * Added check for None decoupler, otherwise null dereferencing when connection failed 53 | 54 | ## Misc 55 | * Updated pipfile to latest dependency versions 56 | -------------------------------------------------------------------------------- /HOW_TO_RELEASE: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | The following packages are needed for the release process: 3 | 4 | - build 5 | - pipenv 6 | - setuptools 7 | - twine 8 | - wheel 9 | 10 | # Steps 11 | ## Update Pipfile.lock 12 | Make sure the Pipfile.lock is up to date with the latest dependencies: 13 | 14 | ``` 15 | python3 -m pipenv update 16 | ``` 17 | 18 | ## Validate tests pass 19 | To release a new version of waapi-client-python, make sure the test suite passes for all versions covered by `tox.ini`. 20 | 21 | ## Bump version 22 | The version is a field in `setup.py`: only bump the major on a breaking change. 23 | 24 | ## Update the CHANGELOG 25 | Make sure to update the CHANGELOG file with the latest changes for that release. 26 | Explain any changes and/or limitation, and the impact it has on users of the library. 27 | 28 | ## Test install from the pypitest server 29 | To test the package installs correctly, first package it with the following command: 30 | 31 | ``` 32 | # Run from clone root, first install the `build` package if you don't have it 33 | python3 -m build --wheel --sdist . 34 | ``` 35 | 36 | Then, upload to the `pypitest` to make sure the install process works. 37 | You will need to first log into test.pypi.org, enable 2-factor authentication and generate an API token at 38 | https://test.pypi.org/manage/account/token/ (or go to your Account Settings, scroll down to the "API tokens" section and 39 | click on "Add API token"). Make it a project-scope token only to waapi-client. 40 | 41 | Once this is done, copy the token and run: 42 | 43 | ``` 44 | # Run from clone root, first install the `twine` package if you don't have it 45 | python3 -m twine upload -r testpypi dist/* 46 | ``` 47 | 48 | When asked for a username, enter `__token__` and paste the token you generated at password input. 49 | 50 | If successful, you can validate the package installs correctly by doing: 51 | ``` 52 | python3 -m pip install -i https://test.pypi.org/simple/ waapi-client 53 | ``` 54 | 55 | ## Upload on the real server 56 | You can do the same as the above on the real server: 57 | 58 | ``` 59 | python3 -m twine upload dist/* 60 | ``` 61 | 62 | Again, confirm the version installs correctly. 63 | 64 | ## Release on Github 65 | Finally, release the code on Github with a version tag. 66 | Tag the HEAD commit of master you uploaded with 67 | 68 | ``` 69 | # Version has a `v` prefix, e.g., v0.7 70 | git tag vVERSION 71 | ``` 72 | 73 | Then push the master branch with tags: 74 | ``` 75 | # Assuming you have a remote called `github` 76 | git push github --tags 77 | ``` 78 | 79 | On Github, create a new release and copy the latest CHANGELOG entry text into the release body. 80 | Releases appear at https://github.com/audiokinetic/waapi-client-python/releases 81 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | autobahn = "*" 8 | 9 | [dev-packages] 10 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "3b8c4f5b039bf4e870cd9902b75347890e275a03fdaba4b191fdcc48a6338e97" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": {}, 8 | "sources": [ 9 | { 10 | "name": "pypi", 11 | "url": "https://pypi.org/simple", 12 | "verify_ssl": true 13 | } 14 | ] 15 | }, 16 | "default": { 17 | "autobahn": { 18 | "hashes": [ 19 | "sha256:ec9421c52a2103364d1ef0468036e6019ee84f71721e86b36fe19ad6966c1181" 20 | ], 21 | "index": "pypi", 22 | "version": "==23.6.2" 23 | }, 24 | "cffi": { 25 | "hashes": [ 26 | "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc", 27 | "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a", 28 | "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417", 29 | "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab", 30 | "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520", 31 | "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36", 32 | "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743", 33 | "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8", 34 | "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed", 35 | "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684", 36 | "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56", 37 | "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324", 38 | "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d", 39 | "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235", 40 | "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e", 41 | "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088", 42 | "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000", 43 | "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7", 44 | "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e", 45 | "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673", 46 | "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c", 47 | "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe", 48 | "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2", 49 | "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098", 50 | "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8", 51 | "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a", 52 | "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0", 53 | "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b", 54 | "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896", 55 | "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e", 56 | "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9", 57 | "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2", 58 | "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b", 59 | "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6", 60 | "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404", 61 | "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f", 62 | "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0", 63 | "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4", 64 | "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc", 65 | "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936", 66 | "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba", 67 | "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872", 68 | "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb", 69 | "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614", 70 | "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1", 71 | "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d", 72 | "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969", 73 | "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b", 74 | "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4", 75 | "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627", 76 | "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956", 77 | "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357" 78 | ], 79 | "markers": "platform_python_implementation != 'PyPy'", 80 | "version": "==1.16.0" 81 | }, 82 | "cryptography": { 83 | "hashes": [ 84 | "sha256:0a68bfcf57a6887818307600c3c0ebc3f62fbb6ccad2240aa21887cda1f8df1b", 85 | "sha256:146e971e92a6dd042214b537a726c9750496128453146ab0ee8971a0299dc9bd", 86 | "sha256:14e4b909373bc5bf1095311fa0f7fcabf2d1a160ca13f1e9e467be1ac4cbdf94", 87 | "sha256:206aaf42e031b93f86ad60f9f5d9da1b09164f25488238ac1dc488334eb5e221", 88 | "sha256:3005166a39b70c8b94455fdbe78d87a444da31ff70de3331cdec2c568cf25b7e", 89 | "sha256:324721d93b998cb7367f1e6897370644751e5580ff9b370c0a50dc60a2003513", 90 | "sha256:33588310b5c886dfb87dba5f013b8d27df7ffd31dc753775342a1e5ab139e59d", 91 | "sha256:35cf6ed4c38f054478a9df14f03c1169bb14bd98f0b1705751079b25e1cb58bc", 92 | "sha256:3ca482ea80626048975360c8e62be3ceb0f11803180b73163acd24bf014133a0", 93 | "sha256:56ce0c106d5c3fec1038c3cca3d55ac320a5be1b44bf15116732d0bc716979a2", 94 | "sha256:5a217bca51f3b91971400890905a9323ad805838ca3fa1e202a01844f485ee87", 95 | "sha256:678cfa0d1e72ef41d48993a7be75a76b0725d29b820ff3cfd606a5b2b33fda01", 96 | "sha256:69fd009a325cad6fbfd5b04c711a4da563c6c4854fc4c9544bff3088387c77c0", 97 | "sha256:6cf9b76d6e93c62114bd19485e5cb003115c134cf9ce91f8ac924c44f8c8c3f4", 98 | "sha256:74f18a4c8ca04134d2052a140322002fef535c99cdbc2a6afc18a8024d5c9d5b", 99 | "sha256:85f759ed59ffd1d0baad296e72780aa62ff8a71f94dc1ab340386a1207d0ea81", 100 | "sha256:87086eae86a700307b544625e3ba11cc600c3c0ef8ab97b0fda0705d6db3d4e3", 101 | "sha256:8814722cffcfd1fbd91edd9f3451b88a8f26a5fd41b28c1c9193949d1c689dc4", 102 | "sha256:8fedec73d590fd30c4e3f0d0f4bc961aeca8390c72f3eaa1a0874d180e868ddf", 103 | "sha256:9515ea7f596c8092fdc9902627e51b23a75daa2c7815ed5aa8cf4f07469212ec", 104 | "sha256:988b738f56c665366b1e4bfd9045c3efae89ee366ca3839cd5af53eaa1401bce", 105 | "sha256:a2a8d873667e4fd2f34aedab02ba500b824692c6542e017075a2efc38f60a4c0", 106 | "sha256:bd7cf7a8d9f34cc67220f1195884151426ce616fdc8285df9054bfa10135925f", 107 | "sha256:bdce70e562c69bb089523e75ef1d9625b7417c6297a76ac27b1b8b1eb51b7d0f", 108 | "sha256:be14b31eb3a293fc6e6aa2807c8a3224c71426f7c4e3639ccf1a2f3ffd6df8c3", 109 | "sha256:be41b0c7366e5549265adf2145135dca107718fa44b6e418dc7499cfff6b4689", 110 | "sha256:c310767268d88803b653fffe6d6f2f17bb9d49ffceb8d70aed50ad45ea49ab08", 111 | "sha256:c58115384bdcfe9c7f644c72f10f6f42bed7cf59f7b52fe1bf7ae0a622b3a139", 112 | "sha256:c640b0ef54138fde761ec99a6c7dc4ce05e80420262c20fa239e694ca371d434", 113 | "sha256:ca20550bb590db16223eb9ccc5852335b48b8f597e2f6f0878bbfd9e7314eb17", 114 | "sha256:d97aae66b7de41cdf5b12087b5509e4e9805ed6f562406dfcf60e8481a9a28f8", 115 | "sha256:e9326ca78111e4c645f7e49cbce4ed2f3f85e17b61a563328c85a5208cf34440" 116 | ], 117 | "markers": "python_version >= '3.7'", 118 | "version": "==42.0.0" 119 | }, 120 | "hyperlink": { 121 | "hashes": [ 122 | "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b", 123 | "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4" 124 | ], 125 | "version": "==21.0.0" 126 | }, 127 | "idna": { 128 | "hashes": [ 129 | "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", 130 | "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" 131 | ], 132 | "markers": "python_version >= '3.5'", 133 | "version": "==3.6" 134 | }, 135 | "pycparser": { 136 | "hashes": [ 137 | "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", 138 | "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" 139 | ], 140 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 141 | "version": "==2.21" 142 | }, 143 | "setuptools": { 144 | "hashes": [ 145 | "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05", 146 | "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78" 147 | ], 148 | "markers": "python_version >= '3.8'", 149 | "version": "==69.0.3" 150 | }, 151 | "txaio": { 152 | "hashes": [ 153 | "sha256:aaea42f8aad50e0ecfb976130ada140797e9dcb85fad2cf72b0f37f8cefcb490", 154 | "sha256:f9a9216e976e5e3246dfd112ad7ad55ca915606b60b84a757ac769bd404ff704" 155 | ], 156 | "markers": "python_version >= '3.7'", 157 | "version": "==23.1.1" 158 | } 159 | }, 160 | "develop": {} 161 | } 162 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wwise Authoring API (Waapi) Client for Python 2 | Decoupled autobahn WAMP client with support for plain options and bindable subscription callbacks. 3 | 4 | ## Requirements 5 | * Non-EOL Python 3.x version (see `tox.ini` for versions tested). Refer to the official [Status of Python versions](https://devguide.python.org/versions) 6 | * [Wwise](https://www.audiokinetic.com/en/download) instance with the Wwise Authoring API enabled (`Project > User Preferences... > Enable Wwise Authoring API`) 7 | 8 | ## Setup 9 | On Windows, it is recommended to use the [Python Launcher for Windows](https://docs.python.org/3/using/windows.html#launcher) which is installed with Python 3 from [python.org](https://www.python.org). 10 | 11 | * Windows: `py -3 -m pip install waapi-client` 12 | * Other platforms: `python3 -m pip install waapi-client` 13 | 14 | ## Usage 15 | ```python 16 | from waapi import WaapiClient 17 | 18 | with WaapiClient() as client: 19 | result = client.call("ak.wwise.core.getInfo") 20 | ``` 21 | 22 | The `with` statement automatically closes the connection and unregisters subscribers. 23 | To keep the connection alive, instantiate `WaapiClient` and call `disconnect` when you are done. 24 | 25 | ```python 26 | from waapi import WaapiClient 27 | 28 | # Connect (default URL) 29 | client = WaapiClient() 30 | 31 | # RPC 32 | result = client.call("ak.wwise.core.getInfo") 33 | 34 | # Subscribe 35 | handler = client.subscribe( 36 | "ak.wwise.core.object.created", 37 | lambda object: print("Object created: " + str(object)) 38 | ) 39 | 40 | # Bind a different callback at any time 41 | def my_callback(object): 42 | print("Different callback: " + str(object)) 43 | 44 | handler.bind(my_callback) 45 | 46 | # Unsubscribe 47 | handler.unsubscribe() 48 | 49 | # Disconnect 50 | client.disconnect() 51 | ``` 52 | 53 | Be aware that failing to call `disconnect` will result in the program to appear unresponsive, as the background thread 54 | running the connection will remain active. 55 | 56 | ## Contribute 57 | This repository accepts pull requests. 58 | You may open an [issue](https://github.com/audiokinetic/waapi-client-python/issues) for any bugs or improvement requests. 59 | 60 | ### Local Install 61 | You may install the package locally using either pip or pipenv. 62 | 63 | Clone this repository, then from the repository root run: 64 | 65 | * Windows: `py -3 -m pip install -e .` 66 | * Other platforms: `python3 -m pip install -e .` 67 | 68 | or 69 | 70 | `pipenv install --three` 71 | 72 | ### Running the Tests 73 | Install the `tox` package: 74 | 75 | * Windows: `py -3 -m pip install tox` 76 | * Other platforms: `python3 -m pip install tox` 77 | 78 | Open a blank project in Wwise, then you may execute `tox` in the terminal from the root of the repository 79 | 80 | The test suite will run for all supported versions of Python. 81 | Use `-e pyXX` to run for a single version, e.g., `tox -e py312` for Python 3.12). -------------------------------------------------------------------------------- /package.bat: -------------------------------------------------------------------------------- 1 | py -3 -m pip install --user --upgrade setuptools wheel build 2 | py -3 -m build --wheel --sdist . 3 | -------------------------------------------------------------------------------- /run_tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from configparser import ConfigParser 4 | from tempfile import TemporaryDirectory 5 | 6 | import argparse 7 | import os 8 | import platform 9 | import subprocess 10 | import sys 11 | import time 12 | 13 | script_dir = os.path.dirname(os.path.realpath(__file__)) 14 | 15 | tox_config = ConfigParser() 16 | 17 | if not tox_config.read(os.path.join(script_dir, "tox.ini")): 18 | print("Error: tox.ini was not found", file=sys.stderr) 19 | sys.exit(1) 20 | python_version_choices = tox_config["tox"]["envlist"].split(",") 21 | 22 | wwiseroot_env = os.environ["WWISEROOT"] if "WWISEROOT" in os.environ else None 23 | 24 | parser = argparse.ArgumentParser() 25 | optional_arguments = parser.add_argument_group("Optional named arguments.") 26 | optional_arguments.add_argument("-p", "--python-versions", help="Python version to run", nargs="+", choices=python_version_choices) 27 | optional_arguments.add_argument("-w", "--wwiseroot-path", required=False, help="Override the path to the Wwise Root") 28 | optional_arguments.add_argument("-c", "--console-path", required=False, help="Override the path to the WwiseConsole executable") 29 | args = parser.parse_args() 30 | 31 | print(f"Running tests on {args.python_versions}.") 32 | 33 | tox_results = [] 34 | 35 | wwiseconsole_path = None 36 | if args.console_path: 37 | wwiseconsole_path = args.console_path 38 | else: 39 | wwiseroot = args.wwiseroot_path or wwiseroot_env 40 | 41 | if not wwiseroot: 42 | print("Error: Cannot find WwiseConsole executable", file=sys.stderr) 43 | print("Pass --console-path or --wwiseroot-path, or define the WWISEROOT environment variable") 44 | sys.exit(1) 45 | 46 | platform_roots = { 47 | "Windows": [os.path.join("Authoring", "x64", "Release", "bin", "WwiseConsole.exe")], 48 | "Darwin": [os.path.join("Authoring", "macosx_gmake", "Release", "bin", "WwiseConsole"), os.path.join("Authoring", "macosx_xcode4", "Release", "bin", "WwiseConsole")], 49 | "Linux": [os.path.join("Authoring", "linux_gmake", "Release", "bin", "WwiseConsole")] 50 | }.get(platform.system()) 51 | 52 | wwiseconsole_path = None 53 | for platform_root in platform_roots: 54 | path = os.path.join(wwiseroot, platform_root) 55 | if os.path.exists(path): 56 | wwiseconsole_path = path 57 | break 58 | 59 | if not wwiseconsole_path: 60 | print("Error: Wwise Authoring was not found", file=sys.stderr) 61 | sys.exit(1) 62 | 63 | with TemporaryDirectory() as temp_dir: 64 | path_new_project = os.path.join(temp_dir, "WaapiClientPython", "WaapiClientPython.wproj") 65 | # Create new wwise project 66 | command = [wwiseconsole_path, "create-new-project", path_new_project, "--platform", "Windows"] 67 | subprocess.run(command, check=False) 68 | 69 | # Open Project 70 | command = [wwiseconsole_path, "waapi-server", path_new_project] 71 | wwiseconsole_project_open = subprocess.Popen(command) 72 | time.sleep(5) 73 | 74 | command = ["tox"] 75 | 76 | # Add the specified python versions 77 | if args.python_versions: 78 | command += [f"-e {python_version}" for python_version in args.python_versions] 79 | 80 | tox_result = subprocess.run(command, check=False) 81 | wwiseconsole_project_open.terminate() 82 | wwiseconsole_project_open.wait(30) # wait for 30 seconds max 83 | 84 | sys.exit(tox_result.returncode) 85 | -------------------------------------------------------------------------------- /samples/interactive_client.py: -------------------------------------------------------------------------------- 1 | import time 2 | from pprint import pprint 3 | 4 | from waapi import EventHandler, connect 5 | 6 | 7 | def main(): 8 | client = connect() 9 | 10 | while not client: 11 | print("Cannot connect, retrying in 1 second...") 12 | time.sleep(1) 13 | client = connect() 14 | 15 | assert(client.is_connected()) 16 | 17 | print("Ready!") 18 | 19 | event_handlers = [] 20 | 21 | while True: 22 | todo = input("What do you want to do? ") 23 | 24 | todo = todo.lower() 25 | if todo == "help": 26 | print("Defined commands:") 27 | print("* getinfo") 28 | print("* sel") 29 | print("* project") 30 | print("* note(w)") 31 | print("* unsub") 32 | print("* quit/exit") 33 | elif todo == "getinfo": 34 | response = client.call("ak.wwise.core.getInfo") 35 | pprint(response) 36 | elif todo == "sel": 37 | def sel_changed(**kwargs): 38 | print("Selection changed!") 39 | 40 | handler = client.subscribe("ak.wwise.ui.selectionChanged", sel_changed) 41 | print("Subscribe succeeded" if handler else "Subscribe failed!") 42 | print(handler) 43 | event_handlers.append(handler) 44 | 45 | elif todo == "project": 46 | myargs = { 47 | "from": { 48 | "ofType": [ 49 | "Project" 50 | ] 51 | }, 52 | "options": { 53 | "return": [ 54 | "name", 55 | "filePath", 56 | "workunit:isDirty" 57 | ] 58 | } 59 | } 60 | response = client.call("ak.wwise.core.object.get", **myargs) 61 | pprint(response) 62 | elif todo.startswith("note"): 63 | def notes_changed_unwrapped(object, newNotes, oldNotes): 64 | print("Notes changed (callback with unwrapped)!") 65 | pprint(object) 66 | pprint(newNotes) 67 | pprint(oldNotes) 68 | 69 | def notes_changed_wrapped(**kwargs): 70 | print("Notes changed (callback with wrapped)!") 71 | pprint(kwargs) 72 | 73 | handler = client.subscribe( 74 | "ak.wwise.core.object.notesChanged", 75 | notes_changed_wrapped if todo.endswith("w") else notes_changed_unwrapped, 76 | **{"return": ["name"]} 77 | ) 78 | print("Subscribe succeeded" if handler else "Subscribe failed!") 79 | print(handler) 80 | event_handlers.append(handler) 81 | elif todo == "unsub": 82 | for sub in client.subscriptions(): 83 | res = sub.unsubscribe() # or client.unsubscribe(sub) 84 | print(("Successfully unsubscribed" if res else "Failed to unsubscribe") + " from " + str(sub)) 85 | 86 | elif todo == "eh": # Event Handler subclass 87 | class MyEventHandler(EventHandler): 88 | def on_event(self, *args, **kwargs): 89 | print("MyEventHandler callback!") 90 | 91 | print("Testing callback...") 92 | event_handler = MyEventHandler() 93 | event_handler() 94 | print("Testing done.") 95 | handler = client.subscribe("ak.wwise.core.object.created", event_handler) 96 | print(("Successfully subscribed" if handler else "Failed to subscribe") + " to ak.wwise.core.object.created") 97 | 98 | elif todo == "rebind": 99 | def rebound_callback(*args, **kwargs): 100 | print("Rebound placeholder callback!") 101 | pprint(args) 102 | pprint(kwargs) 103 | 104 | for sub in client.subscriptions(): 105 | sub.bind(rebound_callback) 106 | 107 | elif todo == "quit" or todo == "exit": 108 | break 109 | 110 | print("Done!") 111 | client.disconnect() 112 | print("Disconnected!") 113 | 114 | 115 | if __name__ == "__main__": 116 | main() 117 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description_file = README.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from distutils.core import setup 3 | 4 | from setuptools import find_packages 5 | 6 | # Dump the README.md file 7 | with open(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'README.md')) as f: 8 | long_description = f.read() 9 | 10 | setup( 11 | name='waapi-client', 12 | packages=find_packages(exclude=['waapi.test']), 13 | test_suite='waapi.test', 14 | install_requires=[ 15 | 'autobahn' 16 | ], 17 | license='Apache License 2.0', 18 | platforms=['any'], 19 | scripts=[], 20 | version='0.7.2', 21 | description='Wwise Authoring API client.', 22 | long_description=long_description, 23 | long_description_content_type="text/markdown", 24 | author='Audiokinetic', 25 | maintainer='Samuel Longchamps', 26 | maintainer_email='slongchamps@audiokinetic.com', 27 | url='https://github.com/audiokinetic/waapi-client-python', 28 | download_url='https://github.com/audiokinetic/waapi-client-python/releases', 29 | keywords=['waapi', 'wwise', 'audiokinetic'], 30 | classifiers=[ 31 | 'Intended Audience :: Developers', 32 | 'Programming Language :: Python :: 3 :: Only', 33 | 'License :: OSI Approved :: Apache Software License', 34 | 'Topic :: Software Development :: Libraries :: Python Modules', 35 | 'Development Status :: 4 - Beta', 36 | 'Operating System :: Microsoft :: Windows', 37 | 'Operating System :: MacOS :: MacOS X', 38 | 'Operating System :: POSIX :: Linux' 39 | ], 40 | ) 41 | -------------------------------------------------------------------------------- /setup_pyenv.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pkg_resources 4 | import subprocess 5 | import os 6 | import sys 7 | 8 | script_dir = os.path.dirname(os.path.realpath(__file__)) 9 | 10 | # Set up pyenv local environment, if pyenv is installed 11 | exit_code, versions_string = subprocess.getstatusoutput("pyenv versions --bare") 12 | if exit_code == 0: 13 | installed_versions = [pkg_resources.parse_version(version) for version in versions_string.split("\n")] 14 | if not installed_versions: 15 | print("Error: No version of Python installed in pyenv", file=sys.stderr) 16 | sys.exit(1) 17 | 18 | installed_versions.sort(reverse=True) # Most recent first 19 | 20 | # Enable all python versions 21 | subprocess.run( 22 | ["pyenv", "local", *(str(version) for version in installed_versions)], 23 | cwd=script_dir, 24 | check=False 25 | ) 26 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py38,py39,py310,py311,py312 3 | 4 | [testenv] 5 | commands = discover 6 | deps = discover -------------------------------------------------------------------------------- /waapi/__init__.py: -------------------------------------------------------------------------------- 1 | # Provide client artifacts directly for simplicity of import statements 2 | from waapi.client import * 3 | -------------------------------------------------------------------------------- /waapi/client/__init__.py: -------------------------------------------------------------------------------- 1 | from waapi.client.client import * 2 | from waapi.client.event import * 3 | from waapi.client.executor import * 4 | -------------------------------------------------------------------------------- /waapi/client/client.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from sys import platform, stdout 3 | from copy import copy 4 | 5 | from waapi.client.event import EventHandler 6 | from waapi.client.interface import UnsubscribeHandler 7 | from waapi.client.executor import SequentialThreadExecutor 8 | from waapi.wamp.interface import WampRequest, WampRequestType, CannotConnectToWaapiException, WaapiRequestFailed 9 | from waapi.wamp.async_decoupled_client import WampClientAutobahn 10 | from waapi.wamp.async_compatibility import asyncio 11 | from waapi.wamp.ak_autobahn import start_decoupled_autobahn_client 12 | 13 | 14 | def connect(url=None): 15 | """ 16 | Factory for uniform API across languages. 17 | Noexcept, returns None if cannot connect. 18 | 19 | :param url: URL of the Waapi server, 20 | :return: WaapiClient | None 21 | """ 22 | try: 23 | return WaapiClient(url) 24 | except CannotConnectToWaapiException: 25 | return None 26 | 27 | def enable_debug_log(): 28 | logging.basicConfig(stream=stdout, level=logging.DEBUG) 29 | WampClientAutobahn.enable_debug_log() 30 | 31 | class WaapiClient(UnsubscribeHandler): 32 | """ 33 | Pythonic Wwise Authoring API client with a synchronous looking API. 34 | 35 | Uses asyncio under the hood in a separate thread to which WAMP requests are dispatched. 36 | Use as a normal API for interacting with Wwise, requires no other special setup. 37 | Each subscription to a topic is managed by a EventHandler instance for a reference is kept in this client. 38 | 39 | The lifetime of the connection is the lifetime of the instance. 40 | Creating a global instance will automatically disconnect the client at the end of the program execution. 41 | 42 | Import as: 43 | from waapi import WaapiClient 44 | """ 45 | def __init__(self, 46 | url=None, 47 | allow_exception=False, 48 | callback_executor=SequentialThreadExecutor 49 | ): 50 | """ 51 | :param url: URL of the Wwise Authoring API WAMP server, defaults to ws://127.0.0.1:8080/waapi 52 | :type: str 53 | :param allow_exception: Allow errors on call and subscribe to throw an exception. Default is False. 54 | :type allow_exception: bool 55 | :param callback_executor: Executor strategy for event callbacks 56 | :type callback_executor: CallbackExecutor 57 | :raises: CannotConnectToWaapiException 58 | """ 59 | super(WaapiClient, self).__init__() 60 | 61 | self._url = url or "ws://127.0.0.1:8080/waapi" 62 | self._allow_exception = allow_exception 63 | self._callback_executor = callback_executor 64 | self._client_thread = None 65 | """:type: Thread""" 66 | 67 | self._loop = asyncio.get_event_loop() 68 | if not self._loop.is_running(): 69 | if not self._loop.is_closed(): 70 | self._loop.close() 71 | if platform == 'win32': 72 | # Prefer the ProactorEventLoop event loop on Windows 73 | self._loop = asyncio.ProactorEventLoop() 74 | else: 75 | self._loop = asyncio.new_event_loop() 76 | asyncio.set_event_loop(self._loop) 77 | 78 | self._decoupler = None 79 | """:type: AutobahnClientDecoupler""" 80 | 81 | self._subscriptions = set() 82 | """:type: set[EventHandler]""" 83 | 84 | # Connect on instantiation (RAII idiom) 85 | if not self.__connect(): 86 | raise CannotConnectToWaapiException("Could not connect to " + self._url) 87 | 88 | def __connect(self): 89 | """ 90 | Connect to the Waapi server. 91 | Never call this method directly from anywhere else than the constructor to preserve RAII. 92 | 93 | :return: True if connection succeeded, False otherwise. 94 | :rtype: bool 95 | """ 96 | self._client_thread, self._decoupler = start_decoupled_autobahn_client( 97 | self._url, 98 | self._loop, 99 | WampClientAutobahn, 100 | self._callback_executor(), 101 | self._allow_exception, 102 | queue_size=0 103 | ) 104 | 105 | # Return upon connection success 106 | self._decoupler.wait_for_joined() 107 | 108 | # A failure is indicated by the runner client thread being terminated 109 | return self._client_thread.is_alive() 110 | 111 | def disconnect(self): 112 | """ 113 | Gracefully disconnect from the Waapi server. 114 | 115 | :return: True if the call caused a successful disconnection, False otherwise. 116 | :rtype: bool 117 | """ 118 | if self.is_connected() and self.__do_request(WampRequestType.STOP): 119 | # Wait for the runner thread to gracefully exit and the asyncio loop to close 120 | if self._client_thread.is_alive(): 121 | self._client_thread.join() 122 | 123 | self._subscriptions.clear() # No need to unsubscribe, subscriptions will be dropped anyways 124 | 125 | # Create a new loop for upcoming uses 126 | if asyncio.get_event_loop().is_closed(): 127 | asyncio.set_event_loop(asyncio.new_event_loop()) 128 | 129 | return True 130 | 131 | # Only the caller that truly caused the disconnection return True 132 | return False 133 | 134 | def is_connected(self): 135 | """ 136 | :return: True if the client is connected, False otherwise. 137 | :rtype: bool 138 | """ 139 | return self._decoupler and self._decoupler.has_joined() and self._client_thread.is_alive() 140 | 141 | def call(self, _uri, *args, **kwargs): 142 | """ 143 | Do a Remote Procedure Call (RPC) to the Waapi server. 144 | Arguments can be specified as named arguments (unless the argument is a reserved keyword), e.g.: 145 | client.call("my.function", some_argument="Value") 146 | 147 | To avoid reserved keywords restrictions, you may specify a single dictionary, e.g.: 148 | client.call("my.function", {"some_argument": "Value"}) 149 | 150 | Options are accepted using the named argument options, which can also be in the dictionary, e.g.: 151 | client.call("my.function", some_argument="Value", options={"option1": "Option Value"}) 152 | OR 153 | client.call("my.function", {"some_argument":"Value", "options":{"option1": "Option Value"}}) 154 | OR 155 | client.call("my.function", {"some_argument":"Value"}, options={"option1": "Option Value"}) 156 | 157 | Note that any named arguments passed take precedence on the values of a dictionary passed as 158 | a positional argument. 159 | 160 | :param _uri: URI of the remote procedure to be called 161 | :type _uri: str 162 | :param kwargs: Keyword arguments to be passed, options may be passed using the key "options" 163 | :return: Result from the remote procedure call, None if failed. 164 | :rtype: dict | None 165 | :raises: WaapiRequestFailed 166 | """ 167 | kwargs = self.__merge_args_to_kwargs(args, kwargs) 168 | return self.__do_request(WampRequestType.CALL, _uri, **kwargs) 169 | 170 | def subscribe(self, _uri, callback_or_handler=None, *args, **kwargs): 171 | """ 172 | Subscribe to a topic on the Waapi server. 173 | Named arguments are options to be passed for the subscription. 174 | 175 | Note that the callback will be called from a different thread. 176 | Use threading mechanisms to synchronize your code and avoid race conditions. 177 | 178 | Like the call method, you may pass a dictionary to avoid reserved keywords restrictions, e.g.: 179 | client.subscribe("my.topic", callback, option1="Value", option2="OtherValue") 180 | OR 181 | client.subscribe("my.topic", callback, {"option1": "Value", "option2": "OtherValue"}) 182 | OR 183 | client.subscribe("my.topic", callback, {"option1": "Value"}, option2="OtherValue") 184 | 185 | Note that any named arguments passed take precedence on the values of a dictionary passed as 186 | a positional argument. 187 | 188 | :param _uri: URI of the remote procedure to be called 189 | :type _uri: str 190 | :param callback_or_handler: A callback that will be called when the server publishes on the provided topic. 191 | The instance can be a function with a matching signature or an instance of a 192 | EventHandler (or subclass). 193 | Note: use a generic signature to support any topic: def fct(*args, **kwargs): 194 | :type callback_or_handler: callable | EventHandler 195 | :rtype: EventHandler | None 196 | :raises: WaapiRequestFailed 197 | """ 198 | kwargs = self.__merge_args_to_kwargs(args, kwargs) 199 | 200 | if callback_or_handler is not None and isinstance(callback_or_handler, EventHandler): 201 | event_handler = callback_or_handler 202 | else: 203 | event_handler = EventHandler(self, callback_or_handler) 204 | 205 | subscription = self.__do_request( 206 | WampRequestType.SUBSCRIBE, 207 | _uri, 208 | event_handler.on_event, 209 | **kwargs 210 | ) 211 | if subscription is not None: 212 | event_handler.subscription = subscription 213 | event_handler._unsubscribe_handler = self 214 | self._subscriptions.add(event_handler) 215 | return event_handler 216 | 217 | def unsubscribe(self, event_handler): 218 | """ 219 | Unsubscribe from a topic managed by the passed EventHandler instance. 220 | 221 | Alternatively, you may use the unsubscribe method on the EventHandler directly. 222 | 223 | :param event_handler: Event handler that can be found in this client instance's subscriptions 224 | :type event_handler: EventHandler 225 | :return: True if successfully unsubscribed, False otherwise. 226 | :rtype: bool 227 | """ 228 | if event_handler not in self._subscriptions: 229 | return False 230 | 231 | success = self.__do_request(WampRequestType.UNSUBSCRIBE, subscription=event_handler.subscription) 232 | if success: 233 | self._subscriptions.remove(event_handler) 234 | event_handler.subscription = None 235 | return success 236 | 237 | def subscriptions(self): 238 | """ 239 | :return: A copy of the set of subscriptions belonging to client instance. 240 | :rtype: set[EventHandler] 241 | """ 242 | return copy(self._subscriptions) 243 | 244 | @staticmethod 245 | def __merge_args_to_kwargs(args, kwargs): 246 | """ 247 | Merged a single dictionary passed as argument to a kwargs dictionary, if it exists. 248 | 249 | :type args: tuple[dict] | tuple[] 250 | :param kwargs: dict 251 | :return: Updated kwargs 252 | :rtype: dict 253 | """ 254 | if len(args) > 0 and isinstance(args[0], dict): 255 | kwargs.update(args[0]) 256 | return kwargs 257 | 258 | def __do_request(self, request_type, _uri=None, callback=None, subscription=None, **kwargs): 259 | """ 260 | Create and forward a generic WAMP request to the decoupler 261 | 262 | :type request_type: WampRequestType 263 | :type _uri: str | None 264 | :type callback: (*Any) -> None | None 265 | :type subscription: Subscription | None 266 | :return: Result from WampRequest, None if request failed. 267 | :rtype: dict | None 268 | """ 269 | if not self._client_thread.is_alive(): 270 | return 271 | 272 | # Make sure the current thread has the event loop set 273 | asyncio.set_event_loop(self._loop) 274 | 275 | async def _async_request(future): 276 | """ 277 | :type future: asyncio.Future 278 | """ 279 | request = WampRequest(request_type, _uri, kwargs, callback, subscription, future) 280 | await self._decoupler.put_request(request) 281 | await future # The client worker is responsible for completing the future 282 | 283 | forwarded_future = asyncio.Future() 284 | concurrent_future = asyncio.run_coroutine_threadsafe(_async_request(forwarded_future), self._loop) 285 | self._decoupler.set_caller_future(concurrent_future) 286 | concurrent_future.result() 287 | self._decoupler.set_caller_future(None) 288 | 289 | # If the decoupled client worker never set the future, it failed and/or died and we return None 290 | if forwarded_future.done(): 291 | return forwarded_future.result() 292 | 293 | def __del__(self): 294 | self.disconnect() 295 | 296 | def __enter__(self): 297 | return self 298 | 299 | def __exit__(self, exc_type, exc_val, exc_tb): 300 | self.disconnect() 301 | -------------------------------------------------------------------------------- /waapi/client/event.py: -------------------------------------------------------------------------------- 1 | from autobahn.wamp.request import Subscription 2 | 3 | 4 | class EventHandler: 5 | """ 6 | Manager of a WAMP subscription with a function handler when an event is received and a mean to unsubscribe 7 | 8 | This is an abstraction over an autobahn subscription that provides flexibility in callback routing. 9 | By using the on_event method as the callback, users can change the behavior of this method either by binding a 10 | callback function (which can change at any point in time) or override on_event in a subclass. 11 | 12 | Constructing an instance of this class before subscription is possible, but the subscription and unsubscribe_handler 13 | members will be updated to match the client and their reference must remain unchanged to properly handle ownership. 14 | 15 | An instance of this class is also callable and can therefore be use as if it were a function reference. 16 | """ 17 | def __init__(self, unsubscribe_handler=None, callback=None): 18 | """ 19 | :param unsubscribe_handler: UnsubscribeHandler | None 20 | :param callback: (*Any) -> None | None 21 | """ 22 | self._unsubscribe_handler = unsubscribe_handler 23 | """:type: UnsubscribeHandler""" 24 | 25 | self._callback = callback 26 | 27 | self._subscription = None 28 | """ 29 | :type: Subscription | None 30 | """ 31 | 32 | @property 33 | def subscription(self): 34 | return self._subscription 35 | 36 | @subscription.setter 37 | def subscription(self, value): 38 | if value is None or isinstance(value, Subscription): 39 | self._subscription = value 40 | 41 | def unsubscribe(self): 42 | """ 43 | :return: True if the EventHandler was unsubscribed successfully, False otherwise. 44 | :rtype: bool 45 | """ 46 | if not self._unsubscribe_handler: 47 | return False 48 | 49 | return self._unsubscribe_handler.unsubscribe(self) 50 | 51 | def on_event(self, *args, **kwargs): 52 | """ 53 | Callback on reception of an event related to the subscribed topic 54 | """ 55 | if self._callback: 56 | self._callback(*args, **kwargs) 57 | 58 | def bind(self, callback): 59 | """ 60 | Bind a callable callback to this EventHandler instance that is called by on_event by default. 61 | When subclassing this class, make sure to define the behavior of this function with regards to how on_event 62 | behaves. 63 | 64 | :type callback: callable | None 65 | :return: self if the callback was correctly set, None otherwise. 66 | :rtype: self | None 67 | """ 68 | if callback and callable(callback): 69 | self._callback = callback 70 | return self 71 | 72 | def __call__(self, *args, **kwargs): 73 | """ 74 | Delegate to on_event 75 | """ 76 | self.on_event(*args, **kwargs) 77 | -------------------------------------------------------------------------------- /waapi/client/executor.py: -------------------------------------------------------------------------------- 1 | from threading import Thread 2 | from queue import Queue 3 | 4 | from waapi.client.interface import CallbackExecutor 5 | from waapi.wamp.async_compatibility import asyncio 6 | 7 | 8 | class PerCallbackThreadExecutor(CallbackExecutor): 9 | def execute(self, callback, kwargs): 10 | Thread(target=lambda: callback(**kwargs)).start() 11 | 12 | class SequentialThreadExecutor(CallbackExecutor): 13 | class ThreadQueuePoison: 14 | pass 15 | poison = ThreadQueuePoison() 16 | publish_queue = Queue() 17 | 18 | @classmethod 19 | def sequential_executor(cls): 20 | while True: 21 | publish = cls.publish_queue.get() 22 | if publish is cls.poison: 23 | break 24 | publish() 25 | 26 | def start(self): 27 | Thread(target=SequentialThreadExecutor.sequential_executor).start() 28 | 29 | def stop(self): 30 | SequentialThreadExecutor.publish_queue.put(SequentialThreadExecutor.poison) 31 | 32 | def execute(self, callback, kwargs): 33 | self.publish_queue.put(lambda: callback(**kwargs)) 34 | 35 | class AsyncioLoopExecutor(CallbackExecutor): 36 | def execute(self, callback, kwargs): 37 | if asyncio.get_event_loop().is_running(): 38 | handler_future = asyncio.Future() 39 | async def _async_request(cb, future): 40 | """ 41 | :type future: asyncio.Future 42 | """ 43 | cb(**kwargs) 44 | future.set_result(True) 45 | 46 | # Run as a coroutine on the asyncio loop 47 | concurrent_future = asyncio.run_coroutine_threadsafe( 48 | _async_request(callback, handler_future), 49 | asyncio.get_event_loop() 50 | ) 51 | -------------------------------------------------------------------------------- /waapi/client/interface.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | 3 | class UnsubscribeHandler: 4 | """ 5 | Abstract service that allows to unsubscribe a EventHandler's subscription 6 | """ 7 | @abstractmethod 8 | def unsubscribe(self, event_handler): 9 | """ 10 | :type event_handler: EventHandler 11 | :return: True if the unsubscribe was successful, False otherwise. 12 | :rtype: bool 13 | """ 14 | raise NotImplementedError() 15 | 16 | class CallbackExecutor: 17 | """ 18 | Abstract executor for a wamp callback, used as a strategy in event handlers 19 | """ 20 | def start(self): 21 | pass 22 | 23 | def stop(self): 24 | pass 25 | 26 | @abstractmethod 27 | def execute(callback): 28 | """ 29 | Executes a callback according to the implemented strategy 30 | Does not guarantee the callback will have been executed upon return 31 | :type callback: () -> Any 32 | """ 33 | raise NotImplementedError() 34 | -------------------------------------------------------------------------------- /waapi/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/audiokinetic/waapi-client-python/ac1f9b7ddb6b2188c80f50bfe96010c40324c673/waapi/test/__init__.py -------------------------------------------------------------------------------- /waapi/test/fixture.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from waapi import WaapiClient 4 | 5 | 6 | class ConnectedClientTestCase(unittest.TestCase): 7 | TIMEOUT_VALUE = 5 # seconds 8 | 9 | @classmethod 10 | def setUpClass(cls): 11 | cls.client = WaapiClient() 12 | 13 | @classmethod 14 | def tearDownClass(cls): 15 | cls.client.disconnect() 16 | 17 | def _create_object(self, name=None): 18 | return self.client.call( 19 | "ak.wwise.core.object.create", 20 | parent="\\Actor-Mixer Hierarchy\\Default Work Unit", 21 | type="Sound", 22 | name=name or "Some Name" 23 | ) 24 | 25 | def _delete_object(self, name=None): 26 | return self.client.call( 27 | "ak.wwise.core.object.delete", 28 | object=f"\\Actor-Mixer Hierarchy\\Default Work Unit\\{name or 'Some Name'}" 29 | ) 30 | 31 | def _delete_objects_if_exists(self, names=None): 32 | names = names or ["Some Name"] 33 | result = self.client.call( 34 | "ak.wwise.core.object.get", { 35 | "from": { 36 | "path": [ 37 | "\\Actor-Mixer Hierarchy\\Default Work Unit" 38 | ] 39 | }, 40 | "transform": [ 41 | {"select": ["children"]} 42 | ] 43 | } 44 | ) 45 | objects = result.get("return", {}) 46 | for obj in objects: 47 | object_name = obj.get("name") 48 | if obj.get("name") in names: 49 | self._delete_object(object_name) 50 | 51 | class CleanConnectedClientTestCase(ConnectedClientTestCase): 52 | 53 | def setUp(self): 54 | self.assertEqual(len(self.client.subscriptions()), 0) 55 | 56 | def tearDown(self): 57 | # Make sure there is no subscriptions before any test 58 | for sub in self.client.subscriptions(): 59 | self.assertTrue(sub.unsubscribe()) 60 | -------------------------------------------------------------------------------- /waapi/test/test_allowed_exception.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from waapi import WaapiClient, WaapiRequestFailed 4 | 5 | 6 | class AllowedException(unittest.TestCase): 7 | def test_exception_on_unknown_uri(self): 8 | with WaapiClient(allow_exception=True) as client: 9 | try: 10 | client.call("i.dont.exist", someArg=True) 11 | except WaapiRequestFailed as e: 12 | self.assertEqual(e.kwargs.get("message"), "The procedure URI is unknown.") 13 | return 14 | 15 | self.fail("Should have thrown an exception") 16 | 17 | def test_exception_on_invalid_argument(self): 18 | with WaapiClient(allow_exception=True) as client: 19 | try: 20 | client.call("ak.wwise.core.getInfo", someArg=True) 21 | except WaapiRequestFailed as e: 22 | self.assertEqual(e.kwargs.get("details", {}).get("typeUri", ""), "ak.wwise.schema_validation_failed") 23 | return 24 | 25 | self.fail("Should have thrown an exception") 26 | -------------------------------------------------------------------------------- /waapi/test/test_connection.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from waapi import WaapiClient, connect, CannotConnectToWaapiException 4 | 5 | 6 | class Connection(unittest.TestCase): 7 | def test_connect_constructor(self): 8 | client = WaapiClient() 9 | self.assertIsNotNone(client) 10 | self.assertTrue(client.is_connected()) 11 | # Implicit disconnect through __del__ 12 | 13 | def test_connect_function(self): 14 | client = connect() 15 | self.assertIsNotNone(client) 16 | self.assertTrue(client.is_connected()) 17 | # Implicit disconnect through __del__ 18 | 19 | def test_disconnect(self): 20 | client = WaapiClient() 21 | self.assertTrue(client.is_connected()) 22 | client.disconnect() 23 | self.assertFalse(client.is_connected()) 24 | 25 | def test_cannot_connect(self): 26 | client = connect("ws://bad_address/waapi") 27 | self.assertIsNone(client) 28 | 29 | def test_with_statement(self): 30 | with WaapiClient() as client: 31 | self.assertTrue(client.is_connected()) 32 | # Implicit disconnect through __exit__ 33 | self.assertFalse(client.is_connected()) 34 | 35 | def test_with_statement_cannot_connect(self): 36 | bad_address = "ws://bad_address/waapi" 37 | try: 38 | with WaapiClient(bad_address) as client: 39 | self.fail("Should not reach this part of the code") 40 | except CannotConnectToWaapiException as e: 41 | # Validate the message indicate the incorrect URL 42 | self.assertIn(bad_address, str(e)) 43 | except Exception: 44 | self.fail("Should not throw any other error types") 45 | -------------------------------------------------------------------------------- /waapi/test/test_executor.py: -------------------------------------------------------------------------------- 1 | from threading import Event 2 | 3 | from waapi.test.fixture import CleanConnectedClientTestCase 4 | from waapi import WaapiClient, \ 5 | PerCallbackThreadExecutor, \ 6 | SequentialThreadExecutor, \ 7 | AsyncioLoopExecutor 8 | 9 | 10 | def _executor_test(self): 11 | expected = [str(i) for i in range(5)] 12 | result = [] 13 | 14 | self._delete_objects_if_exists(expected) 15 | event = Event() 16 | 17 | def created(*args, **kwargs): 18 | nonlocal result 19 | result.append(kwargs.get("newName")) 20 | if len(result) == len(expected): 21 | event.set() 22 | 23 | self.client.subscribe("ak.wwise.core.object.nameChanged", created) 24 | for name in expected: 25 | self._create_object(name) 26 | 27 | self.assertTrue(event.wait(self.TIMEOUT_VALUE)) 28 | self.assertListEqual(expected, result) 29 | 30 | for name in expected: 31 | self._delete_object(name) 32 | 33 | class PerCallbackThreadClient(CleanConnectedClientTestCase): 34 | @classmethod 35 | def setUpClass(cls): 36 | cls.client = WaapiClient(callback_executor=PerCallbackThreadExecutor) 37 | 38 | def test_subscribe(self): 39 | _executor_test(self) 40 | 41 | class SequentialThreadClient(CleanConnectedClientTestCase): 42 | @classmethod 43 | def setUpClass(cls): 44 | cls.client = WaapiClient(callback_executor=SequentialThreadExecutor) 45 | 46 | def test_subscribe(self): 47 | _executor_test(self) 48 | 49 | class AsyncioLoopClient(CleanConnectedClientTestCase): 50 | @classmethod 51 | def setUpClass(cls): 52 | cls.client = WaapiClient(callback_executor=AsyncioLoopExecutor) 53 | 54 | def test_subscribe(self): 55 | _executor_test(self) 56 | -------------------------------------------------------------------------------- /waapi/test/test_integration.py: -------------------------------------------------------------------------------- 1 | from threading import Event 2 | 3 | from waapi.test.fixture import CleanConnectedClientTestCase 4 | 5 | 6 | class Integration(CleanConnectedClientTestCase): 7 | def test_rpc_in_subscribe_callback(self): 8 | self._delete_objects_if_exists() 9 | 10 | event = Event() 11 | handler = None 12 | 13 | def my_callback(object): 14 | # Do a RPC call to delete the object 15 | self.assertIn("id", object) 16 | res = self.client.call("ak.wwise.core.object.delete", object=object.get("id")) 17 | self.assertIsNotNone(res) 18 | self.assertIsInstance(res, dict) 19 | self.assertEqual(len(res), 0) 20 | 21 | self.assertIsNotNone(handler) 22 | self.assertTrue(handler.unsubscribe()) 23 | 24 | event.set() 25 | 26 | handler = self.client.subscribe( 27 | "ak.wwise.core.object.created", 28 | my_callback, 29 | **{"return": ["id"]} 30 | ) 31 | 32 | self.assertIsNotNone(handler) 33 | self._create_object() 34 | self.assertTrue(event.wait(self.TIMEOUT_VALUE)) 35 | -------------------------------------------------------------------------------- /waapi/test/test_large_payload.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from waapi import WaapiClient, connect, CannotConnectToWaapiException 4 | 5 | 6 | class LargePayload(unittest.TestCase): 7 | def test_large_rpc(self): 8 | with WaapiClient() as client: 9 | result = client.call("ak.wwise.core.object.get", { 10 | "from": { 11 | "name": ["GameParameter:a" + str(n) for n in range(5000)], 12 | } 13 | }) 14 | self.assertTrue(client.is_connected()) 15 | 16 | if __name__ == "__main__": 17 | LargePayload().test_large_rpc() -------------------------------------------------------------------------------- /waapi/test/test_rpc_lowlevel.py: -------------------------------------------------------------------------------- 1 | from copy import copy 2 | 3 | from waapi.test.fixture import ConnectedClientTestCase 4 | 5 | class RpcLowLevel(ConnectedClientTestCase): 6 | def test_invalid(self): 7 | result = self.client.call("ak.wwise.idontexist") 8 | self.assertIs(result, None) # Noexcept 9 | 10 | def test_no_argument(self): 11 | result = self.client.call("ak.wwise.core.getInfo") 12 | self.assertIsNotNone(result) 13 | self.assertTrue(isinstance(result, dict)) 14 | self.assertIn("apiVersion", result) 15 | self.assertIn("version", result) 16 | 17 | version = result.get("version") 18 | self.assertIsNotNone(version) 19 | self.assertTrue(isinstance(result, dict)) 20 | self.assertIn("build", version) 21 | self.assertEqual(type(version.get("build")), int) 22 | 23 | def test_with_argument(self): 24 | myargs = { 25 | "from": { 26 | "ofType": [ 27 | "Project" 28 | ] 29 | } 30 | } 31 | result = self.client.call("ak.wwise.core.object.get", **myargs) 32 | self.assertIsNotNone(result) 33 | self.assertTrue(isinstance(result, dict)) 34 | self.assertIn("return", result) 35 | result_return = result.get("return") 36 | 37 | self.assertIsNotNone(result_return) 38 | self.assertTrue(isinstance(result_return, list)) 39 | self.assertEqual(len(result_return), 1) 40 | self.assertTrue(isinstance(result_return[0], dict)) 41 | result_return = result_return[0] 42 | 43 | # Default is (id, name) 44 | self.assertIn("id", result_return) 45 | self.assertIsInstance(result_return.get("id"), str) # GUID 46 | self.assertIsInstance(result_return.get("name"), str) 47 | 48 | def test_with_reserved_uri_argument(self): 49 | myargs = { 50 | "uri": "ak.wwise.core.object.get" 51 | } 52 | result = self.client.call("ak.wwise.waapi.getSchema", **myargs) 53 | self.assertIsNotNone(result) 54 | self.assertTrue(isinstance(result, dict)) 55 | 56 | self.assertEqual(len(result), 3) 57 | self.assertIn("argsSchema", result) 58 | self.assertIn("resultSchema", result) 59 | self.assertIn("optionsSchema", result) 60 | 61 | def test_with_argument_and_return_options(self): 62 | my_args = { 63 | "from": { 64 | "ofType": [ 65 | "Project" 66 | ] 67 | } 68 | } 69 | 70 | my_options = { 71 | "return": [ 72 | "name", 73 | "filePath", 74 | "workunit:isDirty" 75 | ] 76 | } 77 | 78 | def separated_call(): 79 | return self.client.call("ak.wwise.core.object.get", my_args, options=my_options) 80 | 81 | def kwargs_call(): 82 | all_args = copy(my_args) 83 | all_args["options"] = my_options 84 | return self.client.call("ak.wwise.core.object.get", **all_args) 85 | 86 | for call in (separated_call, kwargs_call): 87 | result = call() 88 | self.assertIsNotNone(result) 89 | self.assertTrue(isinstance(result, dict)) 90 | self.assertIn("return", result) 91 | result_return = result.get("return") 92 | result_return = result_return[0] 93 | 94 | self.assertIn("filePath", result_return) 95 | self.assertIsInstance(result_return.get("filePath"), str) 96 | self.assertIn("name", result_return) 97 | self.assertIsInstance(result_return.get("name"), str) 98 | self.assertIn("workunit:isDirty", result_return) 99 | self.assertIsInstance(result_return.get("workunit:isDirty"), bool) 100 | -------------------------------------------------------------------------------- /waapi/test/test_subscribe_lowlevel.py: -------------------------------------------------------------------------------- 1 | from threading import Event 2 | 3 | from waapi import EventHandler 4 | from waapi.test.fixture import CleanConnectedClientTestCase 5 | 6 | 7 | class SubscribeLowLevel(CleanConnectedClientTestCase): 8 | 9 | def test_invalid(self): 10 | handler = self.client.subscribe("ak.wwise.idontexist") 11 | self.assertIs(handler, None) # Noexcept 12 | self.assertEqual(len(self.client.subscriptions()), 0) 13 | 14 | def test_lonely_event_handler(self): 15 | event_handler = EventHandler() 16 | self.assertFalse(event_handler.unsubscribe()) 17 | 18 | # Should do nothing, noexcept. 19 | event_handler.on_event() 20 | event_handler() 21 | 22 | def test_subscribe_no_argument_no_callback(self): 23 | handler = self.client.subscribe("ak.wwise.core.object.nameChanged") 24 | self.assertIsNotNone(handler) 25 | self.assertIsInstance(handler, EventHandler) 26 | self.assertIsNotNone(handler.subscription) 27 | 28 | self.assertEqual(len(self.client.subscriptions()), 1) 29 | self.assertTrue(handler.unsubscribe()) 30 | self.assertEqual(len(self.client.subscriptions()), 0) 31 | 32 | def test_subscribe_no_options_bound_callback(self): 33 | # Precondition: No object 34 | self._delete_objects_if_exists() 35 | 36 | handler = self.client.subscribe("ak.wwise.core.object.created") 37 | 38 | class CallbackCounter: 39 | def __init__(self): 40 | self._count = 0 41 | self._event = Event() 42 | 43 | def wait_for_increment(self): 44 | self._event.wait(CleanConnectedClientTestCase.TIMEOUT_VALUE) 45 | self._event.clear() 46 | 47 | def increment(self, *args, **kwargs): 48 | self._count += 1 49 | self._event.set() 50 | 51 | def count(self): 52 | return self._count 53 | 54 | callback_counter = CallbackCounter() 55 | handler.bind(callback_counter.increment) 56 | 57 | self.assertEqual(callback_counter.count(), 0) 58 | 59 | self.assertIsNotNone(self._create_object()) 60 | 61 | callback_counter.wait_for_increment() 62 | self.assertEqual(callback_counter.count(), 1) 63 | 64 | self.assertIsNotNone(self._delete_object()) 65 | 66 | def test_subscribe_with_options(self): 67 | # Precondition: No object 68 | self._delete_objects_if_exists() 69 | 70 | event = Event() 71 | 72 | def on_object_created(object): 73 | self.assertIn("id", object) 74 | self.assertIn("isPlayable", object) 75 | self.assertIn("classId", object) 76 | 77 | event.set() 78 | 79 | handler = self.client.subscribe( 80 | "ak.wwise.core.object.created", 81 | on_object_created, 82 | { 83 | "return": ["id", "isPlayable", "classId"] 84 | } 85 | ) 86 | 87 | self.assertIsNotNone(self._create_object()) 88 | self.assertTrue(event.wait(self.TIMEOUT_VALUE)) 89 | self.assertIsNotNone(self._delete_object()) 90 | self.assertTrue(handler.unsubscribe()) 91 | 92 | def test_subscribe_lambda(self): 93 | self._delete_objects_if_exists() 94 | 95 | event = Event() 96 | 97 | self.client.subscribe("ak.wwise.core.object.created", lambda object: event.set()) 98 | self._create_object() 99 | 100 | self.assertTrue(event.wait(self.TIMEOUT_VALUE)) 101 | self._delete_object() 102 | 103 | def test_unsubscribed_not_subscribed(self): 104 | handler = self.client.subscribe("ak.wwise.core.object.created") 105 | self.assertIsNotNone(handler) 106 | self.assertTrue(handler.unsubscribe()) 107 | self.assertFalse(handler.unsubscribe()) 108 | 109 | def test_cannot_bind_not_callable(self): 110 | handler = self.client.subscribe("ak.wwise.core.object.created") 111 | self.assertIsNotNone(handler) 112 | self.assertIsNone(handler.bind(None)) 113 | 114 | class NotCallable: 115 | pass 116 | self.assertIsNone(handler.bind(NotCallable())) 117 | 118 | def test_no_callback_valid(self): 119 | self._delete_objects_if_exists() 120 | self.client.subscribe("ak.wwise.core.object.created") 121 | self._create_object() 122 | # No exception: the callback wrapper ignored the publish 123 | self._delete_object() 124 | -------------------------------------------------------------------------------- /waapi/wamp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/audiokinetic/waapi-client-python/ac1f9b7ddb6b2188c80f50bfe96010c40324c673/waapi/wamp/__init__.py -------------------------------------------------------------------------------- /waapi/wamp/ak_autobahn.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import random 3 | import txaio 4 | from sys import stderr 5 | from threading import Thread, Event 6 | from pprint import pformat 7 | 8 | from waapi.wamp.async_compatibility import asyncio 9 | from waapi.wamp.interface import WampRequestType 10 | 11 | from autobahn.asyncio.websocket import WampWebSocketClientFactory 12 | from autobahn.asyncio.wamp import ApplicationSession 13 | 14 | from autobahn.websocket.util import parse_url as parse_ws_url 15 | 16 | from autobahn.wamp import exception, uri 17 | from autobahn.wamp.message import Call, Subscribe 18 | from autobahn.wamp.protocol import CallRequest, is_method_or_function 19 | from autobahn.wamp.request import Handler, SubscribeRequest 20 | from autobahn.wamp.types import SubscribeOptions 21 | 22 | 23 | class AutobahnClientDecoupler: 24 | """ 25 | Decoupler for an autobahn client that indicates when the connection has been made and 26 | manages a queue for requests (WampRequest) 27 | """ 28 | def __init__(self, queue_size): 29 | self._request_queue = asyncio.Queue(queue_size) 30 | self._future = None 31 | self._stopping = False 32 | """:type: concurrent.futures.Future""" 33 | 34 | # Do not use the asyncio loop, otherwise failure to connect will stop 35 | # the loop and the caller will never be notified! 36 | self._joined_event = Event() 37 | 38 | def wait_for_joined(self): 39 | self._joined_event.wait() 40 | 41 | def set_joined(self): 42 | self._joined_event.set() 43 | 44 | def has_joined(self): 45 | return self._joined_event.is_set() 46 | 47 | def put_request(self, request): 48 | """ 49 | Put a WampRequest in the decoupled client processing queue as a coroutine 50 | :type request: WampRequest 51 | :return: Generator that completes when the queue can accept the request 52 | """ 53 | # On first reception of a STOP request, immediately complete other requests with None 54 | if request.request_type == WampRequestType.STOP and not self._stopping: 55 | self._stopping = True 56 | elif self._stopping: 57 | async def stop_now(): 58 | return request.future.set_result(None) 59 | return stop_now() 60 | 61 | return self._request_queue.put(request) 62 | 63 | def get_request(self): 64 | """ 65 | Get a WampRequest from the decoupled client processing queue as a coroutine 66 | :return: Generator to a WampRequest when one is available 67 | """ 68 | return self._request_queue.get() 69 | 70 | def set_caller_future(self, concurrent_future): 71 | self._future = concurrent_future 72 | 73 | def unblock_caller(self): 74 | if self._future: 75 | self._future.set_result(None) 76 | 77 | 78 | def start_decoupled_autobahn_client(url, loop, akcomponent_factory, callback_executor, allow_exception, queue_size): 79 | """ 80 | Initialize a WAMP client runner in a separate thread with the provided asyncio loop 81 | 82 | :type url: str 83 | :type loop: asyncio.AbstractEventLoop 84 | :type akcomponent_factory: (AutobahnClientDecoupler, CallbackExecutor, bool) -> AkComponent 85 | :type callback_executor: CallbackExecutor 86 | :type allow_exception: bool 87 | :type queue_size: int 88 | :rtype: (Thread, AutobahnClientDecoupler) 89 | """ 90 | decoupler = AutobahnClientDecoupler(queue_size) 91 | 92 | async_client_thread = _WampClientThread( 93 | url, 94 | loop, 95 | akcomponent_factory, 96 | callback_executor, 97 | allow_exception, 98 | decoupler 99 | ) 100 | async_client_thread.start() 101 | 102 | return async_client_thread, decoupler 103 | 104 | 105 | class _WampClientThread(Thread): 106 | def __init__(self, url, loop, akcomponent_factory, callback_executor, allow_exception, decoupler): 107 | """ 108 | WAMP client thread that runs the asyncio main event loop 109 | Do NOT terminate this thread to stop the client: use the decoupler to send a STOP request. 110 | 111 | :type url: str 112 | :type loop: asyncio.AbstractEventLoop 113 | :type akcomponent_factory: (AutobahnClientDecoupler, CallbackExecutor, bool) -> AkComponent 114 | :type callback_executor: CallbackExecutor 115 | :type allow_exception: bool 116 | :type decoupler: AutobahnClientDecoupler 117 | """ 118 | super(_WampClientThread, self).__init__() 119 | self._url = url 120 | self._loop = loop 121 | self._decoupler = decoupler 122 | self._akcomponent_factory = akcomponent_factory 123 | self._callback_executor = callback_executor 124 | self._allow_exception = allow_exception 125 | 126 | def run(self): 127 | try: 128 | asyncio.set_event_loop(self._loop) 129 | 130 | txaio.use_asyncio() 131 | txaio.config.loop = self._loop 132 | 133 | # create a WAMP-over-WebSocket transport client factory 134 | transport_factory = WampWebSocketClientFactory( 135 | lambda: self._akcomponent_factory(self._decoupler, self._callback_executor, self._allow_exception), 136 | url=self._url 137 | ) 138 | 139 | # Basic settings with most features disabled 140 | transport_factory.setProtocolOptions( 141 | failByDrop=False, 142 | openHandshakeTimeout=5., 143 | closeHandshakeTimeout=1. 144 | ) 145 | 146 | isSecure, host, port, _, _, _ = parse_ws_url(self._url) 147 | transport, protocol = self._loop.run_until_complete( 148 | self._loop.create_connection( 149 | transport_factory, 150 | host, 151 | port, 152 | ssl=isSecure 153 | ) 154 | ) 155 | 156 | try: 157 | self._loop.run_forever() 158 | except KeyboardInterrupt: 159 | # wait until we send Goodbye if user hit ctrl-c 160 | # (done outside this except so SIGTERM gets the same handling) 161 | pass 162 | 163 | # give Goodbye message a chance to go through, if we still 164 | # have an active session 165 | if protocol._session: 166 | self._loop.run_until_complete(protocol._session.leave()) 167 | 168 | self._loop.close() 169 | except Exception as e: 170 | errorStr = pformat(e) 171 | stderr.write(errorStr + "\n") 172 | 173 | # Wake the caller, this thread will terminate right after so the 174 | # error can be detected by checking if the thread is alive 175 | self._decoupler.set_joined() 176 | 177 | self._decoupler.unblock_caller() 178 | 179 | 180 | class AkCall(Call): 181 | """ 182 | Special implementation with support for custom options 183 | """ 184 | def __init__(self, request, procedure, args=None, kwargs=None): 185 | super(AkCall, self).__init__(request, procedure, args, kwargs) 186 | self.options = kwargs.pop(u"options", {}) 187 | 188 | def marshal(self): 189 | """ 190 | Reimplemented to return a fully formed message with custom options 191 | """ 192 | res = [Call.MESSAGE_TYPE, self.request, self.options, self.procedure, self.args or []] 193 | if self.kwargs: 194 | res.append(self.kwargs) 195 | return res 196 | 197 | 198 | class AkSubscribe(Subscribe): 199 | """ 200 | Special implementation with support for custom options 201 | """ 202 | def __init__(self, request, topic, options=None): 203 | super(AkSubscribe, self).__init__(request, topic) 204 | self.options = options or {} 205 | 206 | def marshal(self): 207 | """ 208 | Reimplemented to return a fully formed message with custom options 209 | """ 210 | return [Subscribe.MESSAGE_TYPE, self.request, self.options, self.topic] 211 | 212 | 213 | class AkComponent(ApplicationSession): 214 | def call(self, procedure, *args, **kwargs): 215 | """ 216 | Reimplemented to support calls with custom options 217 | """ 218 | if not self._transport: 219 | raise exception.TransportLost() 220 | 221 | request_id = random.randint(0, 9007199254740992) 222 | on_reply = txaio.create_future() 223 | self._call_reqs[request_id] = CallRequest(request_id, procedure, on_reply, {}) 224 | 225 | try: 226 | self._transport.send(AkCall(request_id, procedure, args, kwargs)) 227 | except Exception as e: 228 | if request_id in self._call_reqs: 229 | del self._call_reqs[request_id] 230 | raise e 231 | return on_reply 232 | 233 | def _subscribe(self, obj, fn, topic, options): 234 | request_id = self._request_id_gen.next() 235 | on_reply = txaio.create_future() 236 | handler_obj = Handler(fn, obj, None) 237 | self._subscribe_reqs[request_id] = SubscribeRequest(request_id, topic, on_reply, handler_obj) 238 | self._transport.send(AkSubscribe(request_id, topic, options)) 239 | return on_reply 240 | 241 | def subscribe(self, handler, topic=None, options=None): 242 | """ 243 | Implements :func:`autobahn.wamp.interfaces.ISubscriber.subscribe` 244 | """ 245 | assert (topic is None or type(topic) == str) 246 | assert((callable(handler) and topic is not None) or hasattr(handler, '__class__')) 247 | assert (options is None or isinstance(options, dict)) 248 | 249 | if not self._transport: 250 | raise exception.TransportLost() 251 | 252 | if callable(handler): 253 | # subscribe a single handler 254 | return self._subscribe(None, handler, topic, options) 255 | else: 256 | # subscribe all methods on an object decorated with "wamp.subscribe" 257 | on_replies = [] 258 | for k in inspect.getmembers(handler.__class__, is_method_or_function): 259 | proc = k[1] 260 | wampuris = filter(lambda x: x.is_handler(), proc.__dict__.get("_wampuris")) or () 261 | for pat in wampuris: 262 | subopts = pat.options or options or SubscribeOptions( 263 | match=u"wildcard" if pat.uri_type == uri.Pattern.URI_TYPE_WILDCARD else 264 | u"exact").message_attr() 265 | on_replies.append(self._subscribe(handler, proc, pat.uri(), subopts)) 266 | return txaio.gather(on_replies, consume_exceptions=True) 267 | -------------------------------------------------------------------------------- /waapi/wamp/async_compatibility.py: -------------------------------------------------------------------------------- 1 | 2 | import asyncio as _asyncio 3 | 4 | asyncio = _asyncio 5 | 6 | import txaio 7 | txaio.use_asyncio() 8 | -------------------------------------------------------------------------------- /waapi/wamp/async_decoupled_client.py: -------------------------------------------------------------------------------- 1 | from txaio import make_logger 2 | from pprint import pformat 3 | from threading import Thread 4 | 5 | from autobahn.wamp import ApplicationError 6 | 7 | from waapi.client.interface import CallbackExecutor 8 | from waapi.wamp.interface import WampRequestType, WampRequest, WaapiRequestFailed 9 | from waapi.wamp.ak_autobahn import AkComponent 10 | from waapi.wamp.async_compatibility import asyncio 11 | 12 | logger = make_logger() 13 | 14 | class WampClientAutobahn(AkComponent): 15 | """ 16 | Implementation class of a Waapi client using the autobahn library 17 | """ 18 | 19 | def __init__(self, decoupler, callback_executor, allow_exception): 20 | """ 21 | :type decoupler: AutobahnClientDecoupler 22 | :type callback_executor: CallbackExecutor 23 | :param allow_exception: True to allow exception, False to ignore them. 24 | In any case they are logged to stderr. 25 | :type allow_exception: bool 26 | """ 27 | super(WampClientAutobahn, self).__init__() 28 | self._decoupler = decoupler 29 | self._callback_executor = callback_executor 30 | self._allow_exception = allow_exception 31 | 32 | @classmethod 33 | def enable_debug_log(cls): 34 | logger._set_log_level('debug') 35 | 36 | @classmethod 37 | def _log(cls, msg): 38 | logger.debug("WampClientAutobahn: {}".format(msg)) 39 | 40 | async def stop_handler(self, request): 41 | """ 42 | :param request: WampRequest 43 | """ 44 | self._log("Received STOP, stopping and setting the result") 45 | self._callback_executor.stop() 46 | self.disconnect() 47 | self._log("Disconnected") 48 | request.future.set_result(True) 49 | 50 | async def call_handler(self, request): 51 | """ 52 | :param request: WampRequest 53 | """ 54 | self._log("Received CALL, calling " + request.uri) 55 | res = await self.call(request.uri, **request.kwargs) 56 | self._log("Received response for call") 57 | result = res.kwresults if res else {} 58 | if request.callback: 59 | self._log("Callback specified, calling it") 60 | callback = _WampCallbackHandler(request.callback, self._callback_executor) 61 | callback(result) 62 | request.future.set_result(result) 63 | 64 | async def subscribe_handler(self, request): 65 | """ 66 | :param request: WampRequest 67 | """ 68 | self._log("Received SUBSCRIBE, subscribing to " + request.uri) 69 | callback = _WampCallbackHandler(request.callback, self._callback_executor) 70 | subscription = await (self.subscribe( 71 | callback, 72 | topic=request.uri, 73 | options=request.kwargs) 74 | ) 75 | request.future.set_result(subscription) 76 | 77 | async def unsubscribe_handler(self, request): 78 | """ 79 | :param request: WampRequest 80 | """ 81 | self._log("Received UNSUBSCRIBE, unsubscribing from " + str(request.subscription)) 82 | try: 83 | # Successful unsubscribe returns nothing 84 | await request.subscription.unsubscribe() 85 | request.future.set_result(True) 86 | except ApplicationError: 87 | request.future.set_result(False) 88 | except Exception as e: 89 | self._log(str(e)) 90 | request.future.set_result(False) 91 | 92 | async def onJoin(self, details): 93 | self._log("Joined!") 94 | self._decoupler.set_joined() 95 | self._callback_executor.start() 96 | 97 | try: 98 | while True: 99 | self._log("About to wait on the queue") 100 | request = await self._decoupler.get_request() 101 | """:type: WampRequest""" 102 | self._log("Received something!") 103 | 104 | try: 105 | handler = { 106 | WampRequestType.STOP: self.stop_handler, 107 | WampRequestType.CALL: self.call_handler, 108 | WampRequestType.SUBSCRIBE: self.subscribe_handler, 109 | WampRequestType.UNSUBSCRIBE: self.unsubscribe_handler 110 | }.get(request.request_type) 111 | 112 | if handler: 113 | await handler(request) 114 | else: 115 | self._log("Undefined WampRequestType") 116 | except ApplicationError as e: 117 | sanitized_exception_str = str(e).replace("{", "{{").replace("}", "}}") 118 | error_message = "WampClientAutobahn (ERROR): " + pformat(sanitized_exception_str) 119 | logger.error(error_message) 120 | 121 | if self._allow_exception: 122 | request.future.set_exception(WaapiRequestFailed(e)) 123 | else: 124 | request.future.set_result(None) 125 | 126 | self._log("Done treating request") 127 | 128 | if request.request_type == WampRequestType.STOP: 129 | break 130 | 131 | except RuntimeError: 132 | # The loop has been shut down by a disconnect 133 | pass 134 | 135 | def onDisconnect(self): 136 | self._log("The client was disconnected.") 137 | 138 | # Stop the asyncio loop, ultimately stopping the runner thread 139 | asyncio.get_event_loop().stop() 140 | 141 | class _WampCallbackHandler: 142 | """ 143 | Wrapper for a callback that unwraps a WAMP response 144 | """ 145 | def __init__(self, callback, executor): 146 | assert callable(callback) 147 | assert isinstance(executor, CallbackExecutor) 148 | self._callback = callback 149 | self._executor = executor 150 | 151 | def __call__(self, *args, **kwargs): 152 | if self._callback and callable(self._callback): 153 | self._executor.execute(self._callback, kwargs) 154 | -------------------------------------------------------------------------------- /waapi/wamp/interface.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | from autobahn.wamp.request import Subscription 4 | from autobahn.wamp import ApplicationError 5 | 6 | 7 | class CannotConnectToWaapiException(Exception): 8 | pass 9 | 10 | 11 | class WaapiRequestFailed(Exception): 12 | def __init__(self, application_error): 13 | """ 14 | :param application_error: Error coming from the Autobahn library 15 | :type application_error: ApplicationError 16 | """ 17 | self._error = application_error 18 | 19 | @property 20 | def uri(self): 21 | return self._error.error 22 | 23 | @property 24 | def kwargs(self): 25 | return self._error.kwargs 26 | 27 | def __str__(self): 28 | return str(self._error) 29 | 30 | 31 | class WampRequestType(Enum): 32 | STOP = 0, 33 | CALL = 1, 34 | SUBSCRIBE = 2, 35 | UNSUBSCRIBE = 3 36 | 37 | 38 | class WampRequest: 39 | """ 40 | Structure meant to be used as a payload for requests to a WAMP decoupled client 41 | """ 42 | 43 | def __init__(self, request_type, uri=None, kwargs=None, callback=None, subscription=None, future=None): 44 | """ 45 | :type request_type: WampRequestType 46 | :type uri: str | None 47 | :type kwargs: dict | None 48 | :type callback: (*Any) -> None | None 49 | :type subscription: Subscription | None 50 | :param future: Result future to complete upon processing of the request 51 | :type future: asyncio.Future 52 | """ 53 | self.request_type = request_type 54 | self.uri = uri 55 | self.kwargs = kwargs or {} 56 | self.subscription = subscription 57 | self.callback = callback 58 | self.future = future 59 | --------------------------------------------------------------------------------