├── .env ├── .github └── workflows │ └── pythonpublish.yml ├── .gitignore ├── .vs ├── PythonSettings.json ├── VSWorkspaceState.json ├── emitter-python │ └── v16 │ │ └── .suo └── slnx.sqlite ├── LICENSE ├── README.md ├── SampleAppScreenshot.png ├── emitter ├── .vs │ └── emitter-python-sdk │ │ └── v15 │ │ └── .suo ├── __init__.py ├── emitter.py ├── emitter_test.py ├── sample-python2.py ├── sample-python3.py ├── subtrie.py └── subtrie_test.py ├── setup.cfg └── setup.py /.env: -------------------------------------------------------------------------------- 1 | PYTHONPATH=./emitter -------------------------------------------------------------------------------- /.github/workflows/pythonpublish.yml: -------------------------------------------------------------------------------- 1 | # This workflows will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | deploy: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Python 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: '3.x' 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install setuptools wheel twine 25 | - name: Build and publish 26 | env: 27 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 28 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 29 | run: | 30 | python setup.py sdist bdist_wheel 31 | twine upload dist/* 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | dist 3 | emitter_io.egg-info 4 | 5 | *.pyc 6 | .vscode/ 7 | .pytest_cache/ 8 | -------------------------------------------------------------------------------- /.vs/PythonSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "TestFramework": "Pytest" 3 | } -------------------------------------------------------------------------------- /.vs/VSWorkspaceState.json: -------------------------------------------------------------------------------- 1 | { 2 | "ExpandedNodes": [ 3 | "", 4 | "\\emitter" 5 | ], 6 | "SelectedNode": "\\emitter\\sample-python3.py", 7 | "PreviewInSolutionExplorer": false 8 | } -------------------------------------------------------------------------------- /.vs/emitter-python/v16/.suo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emitter-io/python/e23fac64fdc22baae8d28933e6cc06238c1d1b5d/.vs/emitter-python/v16/.suo -------------------------------------------------------------------------------- /.vs/slnx.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emitter-io/python/e23fac64fdc22baae8d28933e6cc06238c1d1b5d/.vs/slnx.sqlite -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Eclipse Public License - v 1.0 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 4 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 5 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 6 | 7 | 1. DEFINITIONS 8 | 9 | "Contribution" means: 10 | 11 | a) in the case of the initial Contributor, the initial code and documentation 12 | distributed under this Agreement, and 13 | b) in the case of each subsequent Contributor: 14 | i) changes to the Program, and 15 | ii) additions to the Program; 16 | 17 | where such changes and/or additions to the Program originate from and are 18 | distributed by that particular Contributor. A Contribution 'originates' 19 | from a Contributor if it was added to the Program by such Contributor 20 | itself or anyone acting on such Contributor's behalf. Contributions do not 21 | include additions to the Program which: (i) are separate modules of 22 | software distributed in conjunction with the Program under their own 23 | license agreement, and (ii) are not derivative works of the Program. 24 | 25 | "Contributor" means any person or entity that distributes the Program. 26 | 27 | "Licensed Patents" mean patent claims licensable by a Contributor which are 28 | necessarily infringed by the use or sale of its Contribution alone or when 29 | combined with the Program. 30 | 31 | "Program" means the Contributions distributed in accordance with this 32 | Agreement. 33 | 34 | "Recipient" means anyone who receives the Program under this Agreement, 35 | including all Contributors. 36 | 37 | 2. GRANT OF RIGHTS 38 | a) Subject to the terms of this Agreement, each Contributor hereby grants 39 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 40 | reproduce, prepare derivative works of, publicly display, publicly 41 | perform, distribute and sublicense the Contribution of such Contributor, 42 | if any, and such derivative works, in source code and object code form. 43 | b) Subject to the terms of this Agreement, each Contributor hereby grants 44 | Recipient a non-exclusive, worldwide, royalty-free patent license under 45 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 46 | transfer the Contribution of such Contributor, if any, in source code and 47 | object code form. This patent license shall apply to the combination of 48 | the Contribution and the Program if, at the time the Contribution is 49 | added by the Contributor, such addition of the Contribution causes such 50 | combination to be covered by the Licensed Patents. The patent license 51 | shall not apply to any other combinations which include the Contribution. 52 | No hardware per se is licensed hereunder. 53 | c) Recipient understands that although each Contributor grants the licenses 54 | to its Contributions set forth herein, no assurances are provided by any 55 | Contributor that the Program does not infringe the patent or other 56 | intellectual property rights of any other entity. Each Contributor 57 | disclaims any liability to Recipient for claims brought by any other 58 | entity based on infringement of intellectual property rights or 59 | otherwise. As a condition to exercising the rights and licenses granted 60 | hereunder, each Recipient hereby assumes sole responsibility to secure 61 | any other intellectual property rights needed, if any. For example, if a 62 | third party patent license is required to allow Recipient to distribute 63 | the Program, it is Recipient's responsibility to acquire that license 64 | before distributing the Program. 65 | d) Each Contributor represents that to its knowledge it has sufficient 66 | copyright rights in its Contribution, if any, to grant the copyright 67 | license set forth in this Agreement. 68 | 69 | 3. REQUIREMENTS 70 | 71 | A Contributor may choose to distribute the Program in object code form under 72 | its own license agreement, provided that: 73 | 74 | a) it complies with the terms and conditions of this Agreement; and 75 | b) its license agreement: 76 | i) effectively disclaims on behalf of all Contributors all warranties 77 | and conditions, express and implied, including warranties or 78 | conditions of title and non-infringement, and implied warranties or 79 | conditions of merchantability and fitness for a particular purpose; 80 | ii) effectively excludes on behalf of all Contributors all liability for 81 | damages, including direct, indirect, special, incidental and 82 | consequential damages, such as lost profits; 83 | iii) states that any provisions which differ from this Agreement are 84 | offered by that Contributor alone and not by any other party; and 85 | iv) states that source code for the Program is available from such 86 | Contributor, and informs licensees how to obtain it in a reasonable 87 | manner on or through a medium customarily used for software exchange. 88 | 89 | When the Program is made available in source code form: 90 | 91 | a) it must be made available under this Agreement; and 92 | b) a copy of this Agreement must be included with each copy of the Program. 93 | Contributors may not remove or alter any copyright notices contained 94 | within the Program. 95 | 96 | Each Contributor must identify itself as the originator of its Contribution, 97 | if 98 | any, in a manner that reasonably allows subsequent Recipients to identify the 99 | originator of the Contribution. 100 | 101 | 4. COMMERCIAL DISTRIBUTION 102 | 103 | Commercial distributors of software may accept certain responsibilities with 104 | respect to end users, business partners and the like. While this license is 105 | intended to facilitate the commercial use of the Program, the Contributor who 106 | includes the Program in a commercial product offering should do so in a manner 107 | which does not create potential liability for other Contributors. Therefore, 108 | if a Contributor includes the Program in a commercial product offering, such 109 | Contributor ("Commercial Contributor") hereby agrees to defend and indemnify 110 | every other Contributor ("Indemnified Contributor") against any losses, 111 | damages and costs (collectively "Losses") arising from claims, lawsuits and 112 | other legal actions brought by a third party against the Indemnified 113 | Contributor to the extent caused by the acts or omissions of such Commercial 114 | Contributor in connection with its distribution of the Program in a commercial 115 | product offering. The obligations in this section do not apply to any claims 116 | or Losses relating to any actual or alleged intellectual property 117 | infringement. In order to qualify, an Indemnified Contributor must: 118 | a) promptly notify the Commercial Contributor in writing of such claim, and 119 | b) allow the Commercial Contributor to control, and cooperate with the 120 | Commercial Contributor in, the defense and any related settlement 121 | negotiations. The Indemnified Contributor may participate in any such claim at 122 | its own expense. 123 | 124 | For example, a Contributor might include the Program in a commercial product 125 | offering, Product X. That Contributor is then a Commercial Contributor. If 126 | that Commercial Contributor then makes performance claims, or offers 127 | warranties related to Product X, those performance claims and warranties are 128 | such Commercial Contributor's responsibility alone. Under this section, the 129 | Commercial Contributor would have to defend claims against the other 130 | Contributors related to those performance claims and warranties, and if a 131 | court requires any other Contributor to pay any damages as a result, the 132 | Commercial Contributor must pay those damages. 133 | 134 | 5. NO WARRANTY 135 | 136 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN 137 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR 138 | IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each 140 | Recipient is solely responsible for determining the appropriateness of using 141 | and distributing the Program and assumes all risks associated with its 142 | exercise of rights under this Agreement , including but not limited to the 143 | risks and costs of program errors, compliance with applicable laws, damage to 144 | or loss of data, programs or equipment, and unavailability or interruption of 145 | operations. 146 | 147 | 6. DISCLAIMER OF LIABILITY 148 | 149 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 150 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 151 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 152 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 153 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 154 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 155 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY 156 | OF SUCH DAMAGES. 157 | 158 | 7. GENERAL 159 | 160 | If any provision of this Agreement is invalid or unenforceable under 161 | applicable law, it shall not affect the validity or enforceability of the 162 | remainder of the terms of this Agreement, and without further action by the 163 | parties hereto, such provision shall be reformed to the minimum extent 164 | necessary to make such provision valid and enforceable. 165 | 166 | If Recipient institutes patent litigation against any entity (including a 167 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 168 | (excluding combinations of the Program with other software or hardware) 169 | infringes such Recipient's patent(s), then such Recipient's rights granted 170 | under Section 2(b) shall terminate as of the date such litigation is filed. 171 | 172 | All Recipient's rights under this Agreement shall terminate if it fails to 173 | comply with any of the material terms or conditions of this Agreement and does 174 | not cure such failure in a reasonable period of time after becoming aware of 175 | such noncompliance. If all Recipient's rights under this Agreement terminate, 176 | Recipient agrees to cease use and distribution of the Program as soon as 177 | reasonably practicable. However, Recipient's obligations under this Agreement 178 | and any licenses granted by Recipient relating to the Program shall continue 179 | and survive. 180 | 181 | Everyone is permitted to copy and distribute copies of this Agreement, but in 182 | order to avoid inconsistency the Agreement is copyrighted and may only be 183 | modified in the following manner. The Agreement Steward reserves the right to 184 | publish new versions (including revisions) of this Agreement from time to 185 | time. No one other than the Agreement Steward has the right to modify this 186 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The 187 | Eclipse Foundation may assign the responsibility to serve as the Agreement 188 | Steward to a suitable separate entity. Each new version of the Agreement will 189 | be given a distinguishing version number. The Program (including 190 | Contributions) may always be distributed subject to the version of the 191 | Agreement under which it was received. In addition, after a new version of the 192 | Agreement is published, Contributor may elect to distribute the Program 193 | (including its Contributions) under the new version. Except as expressly 194 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or 195 | licenses to the intellectual property of any Contributor under this Agreement, 196 | whether expressly, by implication, estoppel or otherwise. All rights in the 197 | Program not expressly granted under this Agreement are reserved. 198 | 199 | This Agreement is governed by the laws of the State of New York and the 200 | intellectual property laws of the United States of America. No party to this 201 | Agreement will bring a legal action under this Agreement more than one year 202 | after the cause of action arose. Each party waives its rights to a jury trial in 203 | any resulting litigation. 204 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Emitter Python SDK 2 | 3 | [![PyPI - Emitter version](https://img.shields.io/pypi/v/emitter-io.svg)](https://pypi.org/project/emitter-io) [![PyPI - Python versions](https://img.shields.io/pypi/pyversions/emitter-io.svg?logo=python)](https://github.com/emitter-io/python) [![GitHub - License](https://img.shields.io/github/license/emitter-io/python.svg)](https://github.com/emitter-io/python/blob/master/LICENSE) 4 | 5 | This repository contains a Python client for [Emitter](https://emitter.io) (see also [Emitter GitHub](https://github.com/emitter-io/emitter)). Emitter is an **open-source** real-time communication service for connecting online devices. At its core, emitter.io is a distributed, scalable and fault-tolerant publish-subscribe messaging platform based on MQTT protocol and featuring message storage. 6 | 7 | This library provides a nicer high-level MQTT interface fine-tuned and extended with specific features provided by [Emitter](https://emitter.io). The code uses the [Eclipse Paho MQTT Python Client](https://github.com/eclipse/paho.mqtt.python) for handling all the network communication and MQTT protocol. 8 | 9 | 10 | * [Installation](#install) 11 | * [Examples](#examples) 12 | * [API reference](#api) 13 | * [ToDo](#todo) 14 | * [License](#license) 15 | 16 | 17 | 18 | ## Installation 19 | 20 | This SDK is available as a pip package. Install with: 21 | ``` 22 | pip install emitter-io 23 | ``` 24 | 25 | 26 | 27 | ## Examples 28 | 29 | These examples show you the whole communication process. 30 | * Python 2: [*sample-python2.py*](emitter/sample-python2.py) 31 | * Python 3: [*sample-python3.py*](emitter/sample-python3.py) 32 | 33 | ![alt screenshot of the sample app](SampleAppScreenshot.png?raw=true) 34 | 35 | 36 | ## API reference 37 | 38 | * [`Client()`](#client) 39 | * [`.connect()`](#connect) 40 | * [`.disconnect()`](#disconnect) 41 | * [`.keyban()`](#keyban) 42 | * [`.keygen()`](#keygen) 43 | * [`.link()`](#link) 44 | * [`.me()`](#me) 45 | * [`.on_connect`](#on_connect) 46 | * [`.on_disconnect`](#on_disconnect) 47 | * [`.on_error`](#on_error) 48 | * [`.on_keyban`](#on_keyban) 49 | * [`.on_keygen`](#on_keygen) 50 | * [`.on_me`](#on_me) 51 | * [`.on_message`](#on_message) 52 | * [`.on_presence`](#on_presence) 53 | * [`.presence()`](#presence) 54 | * [`.publish()`](#publish) 55 | * [`.publish_with_link()`](#publish_with_link) 56 | * [`.subscribe()`](#subscribe) 57 | * [`.subscribe_with_group()`](#subscribe_with_group) 58 | * [`.unsubscribe()`](#unsubscribe) 59 | * [`EmitterMessage()`](#message) 60 | * [`.as_string()`](#as_string) 61 | * [`.as_object()`](#as_object) 62 | * [`.as_binary()`](#as_binary) 63 | 64 | ------------------------------------------------------- 65 | 66 | ### Client() 67 | 68 | The `Client` class represents the client connection to an Emitter server. 69 | 70 | ------------------------------------------------------- 71 | 72 | ### Emitter#connect(host="api.emitter.io", port=443, secure=True, keepalive=30) 73 | 74 | ```python 75 | emitter = Client() 76 | 77 | emitter.connect() 78 | ``` 79 | Connects to an Emitter server. 80 | * `host` is the address of the Emitter broker. (Optional | `Str` | Default: `"api.emitter.io"`) 81 | * `port` is the port of the emitter broker. (Optional | `Int` | Default: `443`) 82 | * `secure` whether the connection should be secure. (Optional | `Bool` | Default: `True`) 83 | * `keepalive` is the time the connection is kept alive (Optional | `Int` | Default: `30`) 84 | 85 | If you don't want a secure connection, set the port to 8080, unless your broker is configured differently. 86 | 87 | To handle connection events, see the [`.on_connect`](#on_connect) property. 88 | 89 | ------------------------------------------------------- 90 | 91 | ### Emitter#disconnect() 92 | 93 | ```python 94 | emitter.disconnect() 95 | ``` 96 | Disconnects from the connected Emitter server. 97 | 98 | To handle disconnection events, see the [`.on_disconnect`](#on_disconnect) property. 99 | 100 | ------------------------------------------------------- 101 | 102 | ### Emitter#keyban(master_key, target_key, ban) 103 | 104 | ```python 105 | instance.keyban("MEj8QNnzy6pKtE887hpXbD0KyKXi4w4f", "ftibXtPMKXI5p2FjhyINf8tvl2GAHaNG", True) 106 | ``` 107 | Sends a request to ban/unban a channel key. 108 | * `master_key` is your *master key* to use for the operation. (Required | `Str`) 109 | * `target_key` is the key ban or unban. (Required | `Str`) 110 | * `ban` is whether to ban or unban the key. (Required | `Bool`) 111 | 112 | To handle keyban responses, see the [`.on_keyban`](#on_keyban) property. 113 | It will take a minute for the change to take effect. 114 | 115 | ------------------------------------------------------- 116 | 117 | ### Emitter#keygen(key, channel, permissions, ttl=0) 118 | 119 | ```python 120 | instance.keygen("Z5auMQhNr0eVnGBAgWThXus1dgtSsvuQ", "channel/", "rwslpex") 121 | ``` 122 | Sends a key generation request to the server. 123 | * `key` is your *master key* to use for the operation. (Required | `Str`) 124 | * `channel` is the channel name to generate a key for. (Required | `Str`) 125 | * `permissions` are the permissions associated to the key. (Required | `Str`) 126 | - `r` for read 127 | - `w` for write 128 | - `s` for store 129 | - `l` for load 130 | - `p` for presence 131 | - `e` for extend 132 | - `x` for execute 133 | * `ttl` is the time to live of the key. `0` means it never expires (Optional | `Int` | Default: `0`) 134 | 135 | To handle keygen responses, see the [`.on_keygen`](#on_keygen) property. 136 | Requesting a keygen with an extendable channel creates a private channel. 137 | 138 | ------------------------------------------------------- 139 | 140 | ### Emitter#link(key, channel, name, private, subscribe, options={}) 141 | 142 | ```python 143 | instance.link("5xZjIQp6GA9fpxso1Kslqnv8d4XVWChb", 144 | "channel", 145 | "a0", 146 | True, 147 | {Client.with_ttl(604800), Client.without_echo()}) // one week 148 | ``` 149 | Sends a link creation request to the server. This allows for the creation of a link between a short 2-character name and an actual channel. This function no longer allows the creation of a private channel. For this, use [`.keygen`](#keygen). 150 | * `key` is the key to the channel. (Required | `Str`) 151 | * `channel` is the channel name. (Required | `Str`) 152 | * `name` is the short name for the channel. (Required | `Str`) 153 | * `subscribe` whether or not to subscribe to the channel. (Required | `Bool`) 154 | * `options` a set of options. Currently available options are: 155 | - `with_at_most_once()` to send with QoS0. 156 | - `with_at_least_once()` to send with QoS1. 157 | - `with_retain()` to retain this message. 158 | - `with_ttl(ttl)` to set a time to live for the message. 159 | - `without_echo()` to tell the broker not to send the message back to this client. 160 | 161 | ------------------------------------------------------- 162 | 163 | ### Emitter#me() 164 | 165 | ```python 166 | instance.me() 167 | ``` 168 | Requests information about the connection. Information provided in the response contains the id of the connection, as well as the links that were established with [`.link()`](#link) requests. 169 | 170 | To handle the responses, see the [`.on_me`](#on_me) property. 171 | 172 | ------------------------------------------------------- 173 | 174 | ### Emitter#on_connect 175 | 176 | Property used to get or set the connection handler, that handle events emitted upon successful (re)connection. No arguments provided. 177 | 178 | ------------------------------------------------------- 179 | 180 | ### Emitter#on_disconnect 181 | 182 | Property used to get or set the disconnection handler, that handle events emitted after a disconnection. No arguments provided. 183 | 184 | ------------------------------------------------------- 185 | 186 | ### Emitter#on_error 187 | 188 | Property used to get or set the error handler, that handle events emitted when an error occurs following any request. The event comes with a status code and a text message describing the error. 189 | 190 | ```json 191 | {"status": 400, 192 | "message": "the request was invalid or cannot be otherwise served"} 193 | ``` 194 | 195 | ------------------------------------------------------- 196 | 197 | ### Emitter#on_keyban 198 | 199 | Property used to get or set the handler for [`.keyban()`](#keyban) requests. Here is a sample of the message received after such a request: 200 | 201 | ``` 202 | {"status": 200, 203 | "banned": True} 204 | ``` 205 | 206 | ------------------------------------------------------- 207 | 208 | ### Emitter#on_keygen 209 | 210 | **ToDo: Description!** 211 | 212 | ------------------------------------------------------- 213 | 214 | ### Emitter#on_me 215 | 216 | Property used to get or set the handler that handle responses to [`.me()`](#me) requests. Information provided in the response contains the id of the connection, as well as the links that were established with [`.link()`](#link) requests. 217 | 218 | ```json 219 | {"id": "74W77OC5OXDBQRUUMSHROHRQPE", 220 | "links": {"a0": "test/", 221 | "a1": "test/"}} 222 | ``` 223 | 224 | ------------------------------------------------------- 225 | 226 | ### Emitter#on_message 227 | 228 | Emitted when the client receives a message packet. The message object will be of [EmitterMessage](#message) class, encapsulating the channel and the payload. 229 | 230 | ------------------------------------------------------- 231 | 232 | ### Emitter#on_presence 233 | 234 | Emitted either when a presence call was made requesting a status, using the [`Emitter#presence()`](#presence) function, or when a user subscribed/unsubscribed to the channel and updates were previously requested using again a call to the [`Emitter#presence()`](#presence) function. Example arguments below. 235 | 236 | ```json 237 | {"time": 1577833210, 238 | "event": "status", 239 | "channel": "", 240 | "who": [{"id": "ABCDE12345FGHIJ678910KLMNO", "username": "User1"}, 241 | {"id": "PQRST12345UVWXY678910ZABCD"}]} 242 | {"time": 1577833220, 243 | "event": "subscribe", 244 | "channel": "", 245 | "who": {"id": "ABCDE12345FGHIJ678910KLMNO", "username": "User1"}} 246 | {"time": 1577833230, 247 | "event": "unsubscribe", 248 | "channel": "", 249 | "who": {"id": "ABCDE12345FGHIJ678910KLMNO"}} 250 | ```` 251 | * `time` is the time of the event as *Unix time*. 252 | * `event` is the event type: `subscribe` when an remote instance subscribed to the channel, `unsubscribe` when an remote instance unsubscribed from the channel and `status` when [`Emitter#presence()`](#presence) is called the first time. 253 | * `channel` is the channel name. 254 | * `who` in case of the `event` is `(un)subscribe` one dict with the user id, when the `event` is `status`, it is a list with the users. When more than 1000 users at the moment subscribed to the channel, 1000 randomly selected are displayed. 255 | * `id` is an internal generated id of the remote instance. 256 | * `username` is a custom chosen name by the remote instance. Please note that it is **optional** and check always if this parameter exists. 257 | 258 | ------------------------------------------------------- 259 | 260 | ### Emitter#presence(key, channel, status=False, changes=False, optional_handler=None) 261 | 262 | ```python 263 | instance.presence(""5xZjIQp6GA9fpxso1Kslqnv8d4XVWChb"", 264 | "channel", 265 | True, 266 | True) 267 | ``` 268 | Sends a presence request to the server. 269 | * `key` is the channel key to use for the operation. (Required | `Str`) 270 | * `channel` is the channel name of which you want to call the presence. (Required | `Str`) 271 | * `status` is whether the broker should send a full status of the channel. (Optional | `Bool` | Default: `False`) 272 | * `changes` is whether to subscribe to presence changes on the channel. (Optional | `Bool` | Default: `False`) 273 | * `optional_handler` is the handler to insert in the handler trie. (Optional | `callable` | Default: `None`) 274 | 275 | Note: if you do not provide a handler here, make sure you did set the default handler for all presence messages using the [`.on_presence`](#on_presence) property. 276 | 277 | ------------------------------------------------------- 278 | 279 | ### Emitter#publish(key, channel, message, options={}) 280 | 281 | ```python 282 | emitter.publish("5xZjIQp6GA9fpxso1Kslqnv8d4XVWChb", 283 | "channel", 284 | "Hello Emitter!", 285 | {Client.with_ttl(604800), Client.without_echo()}) // one week 286 | ``` 287 | Publishes a message to a particual channel. 288 | * `key` is the channel key to use for the operation. (Required | `Str`) 289 | * `channel` is the channel name to publish to. (Required | `Str`) 290 | * `message` is the message to publish (Required | `String`) 291 | * `options` a set of options. Currently available options are: 292 | - `with_at_most_once()` to send with QoS0. 293 | - `with_at_least_once()` to send with QoS1. 294 | - `with_retain()` to retain this message. 295 | - `with_ttl(ttl)` to set a time to live for the message. 296 | - `without_echo()` to tell the broker not to send the message back to this client. 297 | 298 | ------------------------------------------------------- 299 | 300 | ### Emitter#publish_with_link(link, message) 301 | 302 | ```python 303 | instance.publishWithLink("a0", 304 | "Hello Emitter!") 305 | ``` 306 | Sends a message through the link. 307 | * `link` is the 2-character name of the link. (Required | `Str`) 308 | * `message` is the message to send through the link. (Required | `Str`) 309 | 310 | ------------------------------------------------------- 311 | 312 | 313 | ### Emitter#subscribe(key, channel, optional_handler=None, options={}) 314 | 315 | ```python 316 | instance.subscribe("5xZjIQp6GA9fpxso1Kslqnv8d4XVWChb", 317 | "channel", 318 | options={Client.with_last(5)}) 319 | ``` 320 | Subscribes to a particular channel. 321 | * `key` is the channel key to use for the operation. (Required | `Str`) 322 | * `channel` is the channel name to subscribe to. (Required | `Str`) 323 | * `optional_handler` is the handler to insert in the handler trie. (Optional | `callable` | Default: `None`) 324 | * `options` a set of options. Currently available options are: 325 | - `with_last(x)` to receive the last `x` messages stored on the channel. 326 | 327 | TODO 328 | - `with_from` 329 | - `with_until` 330 | 331 | Note: if you do not provide a handler here, make sure you did set the default handler for all messages using the [`.on_message`](#on_message) property. 332 | 333 | ------------------------------------------------------- 334 | 335 | 336 | ### Emitter#subscribe_with_group(key, channel, share_group, optional_handler=None, options={}) 337 | 338 | ```python 339 | instance.subscribe("5xZjIQp6GA9fpxso1Kslqnv8d4XVWChb", 340 | "channel", 341 | "sg") 342 | ``` 343 | Subscribes to a particular share group for a channel. A message sent to that channel will be forwarded to only one member of the share group, chosen randomly. For more information about share groups, see 344 | [Emitter: Load-balance Messages using Subscriber Groups (on YouTube)](https://youtu.be/Vl7iGKEQrTg). 345 | 346 | * `key` is the channel key to use for the operation. (Required | `Str`) 347 | * `channel` is the channel name to subscribe to. (Required | `Str`) 348 | * `share_group` is the name of the group to join. (Required | `Str`) 349 | * `optional_handler` is the handler to insert in the handler trie. (Optional | `callable` | Default: `None`) 350 | * `options` a set of options. 351 | 352 | 353 | Note: if you do not provide a handler here, make sure you did set the default handler for all messages using the [`.on_message`](#on_message) property. 354 | 355 | ------------------------------------------------------- 356 | 357 | ### Emitter#unsubscribe(key, channel) 358 | 359 | ```python 360 | instance.unsubscribe("5xZjIQp6GA9fpxso1Kslqnv8d4XVWChb", 361 | "channel") 362 | ``` 363 | Unsubscribes from a particual channel. 364 | * `key` is the channel key to use for the operation. (Required | `Str`) 365 | * `channel` is the channel name to unsubscribe from. (Required | `Str`) 366 | 367 | This deletes handlers for that channel from the trie. 368 | 369 | ------------------------------------------------------- 370 | 371 | ### EmitterMessage() 372 | 373 | The `EmitterMessage` class represents a message received from the Emitter server. It contains two properties: 374 | * `channel` is the channel name the message was published to. (`Str`) 375 | * `binary` is the buffer associated with the payload. (Binary `Str`) 376 | 377 | ------------------------------------------------------- 378 | 379 | ### EmitterMessage#asString() 380 | 381 | ```python 382 | message.asString() 383 | ``` 384 | Returns the payload as a utf-8 `String`. 385 | 386 | ------------------------------------------------------- 387 | 388 | ### EmitterMessage#asObject() 389 | 390 | ```python 391 | message.asObject() 392 | ``` 393 | Returns the payload as a JSON-deserialized dictionary. 394 | 395 | ------------------------------------------------------- 396 | 397 | ### EmitterMessage#asBinary() 398 | 399 | ```python 400 | message.asBinary() 401 | ``` 402 | Returns the payload as a raw binary buffer. 403 | 404 | 405 | 406 | ## ToDo 407 | 408 | There are some points where the Python libary can be improved: 409 | - Complete the [keygen](#client-keygen) entry in the README (see the **ToDo** markings) 410 | - Describe how to use the trie of handlers for regular messages and presence. 411 | - Add `with_from` and `with_until`. 412 | - asObject should return an actual object instead of a dictionary. 413 | 414 | 415 | ## License 416 | 417 | Eclipse Public License 1.0 (EPL-1.0) 418 | 419 | Copyright (c) 2016-2019 [Misakai Ltd.](http://misakai.com) 420 | -------------------------------------------------------------------------------- /SampleAppScreenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emitter-io/python/e23fac64fdc22baae8d28933e6cc06238c1d1b5d/SampleAppScreenshot.png -------------------------------------------------------------------------------- /emitter/.vs/emitter-python-sdk/v15/.suo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emitter-io/python/e23fac64fdc22baae8d28933e6cc06238c1d1b5d/emitter/.vs/emitter-python-sdk/v15/.suo -------------------------------------------------------------------------------- /emitter/__init__.py: -------------------------------------------------------------------------------- 1 | from .emitter import Client 2 | -------------------------------------------------------------------------------- /emitter/emitter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | This is the Python client for Emitter (emitter.io). 5 | GitHub: github.com/emitter-io/python 6 | License: Eclipse Public License 1.0 (EPL-1.0) 7 | """ 8 | import json 9 | import re 10 | import logging 11 | import ssl 12 | import paho.mqtt.client as mqtt 13 | try: 14 | from .subtrie import SubTrie 15 | except ImportError: 16 | from subtrie import SubTrie 17 | 18 | 19 | class Client(object): 20 | """ 21 | * Represents the client connection to an Emitter server. 22 | """ 23 | 24 | def __init__(self): 25 | """ 26 | * Register the variables for later use. 27 | """ 28 | self._mqtt = None 29 | self._handler_message = None 30 | self._handler_trie_message = SubTrie() 31 | self._handler_connect = None 32 | self._handler_disconnect = None 33 | self._handler_error = None 34 | self._handler_presence = None 35 | self._handler_trie_presence = SubTrie() 36 | self._handler_me = None 37 | self._handler_keygen = None 38 | self._handler_keyban = None 39 | 40 | @property 41 | def on_connect(self): 42 | return self._handler_connect 43 | @on_connect.setter 44 | def on_connect(self, func): 45 | self._handler_connect = func 46 | 47 | @property 48 | def on_disconnect(self): 49 | return self._handler_disconnect 50 | @on_disconnect.setter 51 | def on_disconnect(self, func): 52 | self._handler_disconnect = func 53 | 54 | @property 55 | def on_error(self): 56 | return self._handler_error 57 | @on_error.setter 58 | def on_error(self, func): 59 | self._handler_error = func 60 | 61 | @property 62 | def on_presence(self): 63 | return self._handler_presence 64 | @on_presence.setter 65 | def on_presence(self, func): 66 | self._handler_presence = func 67 | 68 | @property 69 | def on_me(self): 70 | return self._handler_me 71 | @on_me.setter 72 | def on_me(self, func): 73 | self._handler_me = func 74 | 75 | @property 76 | def on_keygen(self): 77 | return self._handler_keygen 78 | @on_keygen.setter 79 | def on_keygen(self, func): 80 | self._handler_keygen = func 81 | 82 | @property 83 | def on_keyban(self): 84 | return self._handler_keyban 85 | @on_keyban.setter 86 | def on_keyban(self, func): 87 | self._handler_keyban = func 88 | 89 | @property 90 | def on_message(self): 91 | return self._handler_message 92 | @on_message.setter 93 | def on_message(self, func): 94 | self._handler_message = func 95 | 96 | def loop(self, timeout): 97 | """ 98 | * Call regularly to process network events. This call waits in select() 99 | * until the network socket is available for reading or writing, if 100 | * appropriate, then handles the incoming/outgoing data. This function 101 | * blocks for up to timeout seconds. 102 | * timeout must not exceed the keepalive value for the client or your 103 | * client will be regularly disconnected by the broker. 104 | """ 105 | self._mqtt.loop(timeout=timeout) 106 | 107 | def loop_forever(self): 108 | """ 109 | * This is a blocking form of the network loop and will not return until the 110 | * client calls disconnect(). It automatically handles reconnecting. 111 | """ 112 | self._mqtt.loop_forever() 113 | 114 | def loop_start(self): 115 | """ 116 | * These functions implement a threaded interface to the network loop. 117 | * Calling loop_start() once, before or after connect*(), runs a thread in 118 | * the background to call loop() automatically. This frees up the main 119 | * thread for other work that may be blocking. This call also handles 120 | * reconnecting to the broker. 121 | """ 122 | self._mqtt.loop_start() 123 | 124 | def loop_stop(self): 125 | """ 126 | * Stops the loop started in loopStart(). 127 | * See loopStart() for more information. 128 | """ 129 | self._mqtt.loop_stop() 130 | 131 | 132 | def _on_connect(self, client, userdata, flags, rc): 133 | """ 134 | * Occurs when connection is established. 135 | """ 136 | if self._handler_connect: 137 | self._handler_connect() 138 | 139 | 140 | def _on_disconnect(self, client, userdata, rc): 141 | """ 142 | * Occurs when the connection was lost. 143 | """ 144 | if self._handler_disconnect: 145 | self._handler_disconnect() 146 | 147 | def _invoke_trie_handlers(self, trie, default_handler, message): 148 | handlers = trie.lookup(message.channel) 149 | if len(handlers) == 0 and default_handler: 150 | default_handler(message) 151 | 152 | for h in handlers: 153 | h(message) 154 | 155 | 156 | def _on_message(self, client, userdata, msg): 157 | message = EmitterMessage(msg) 158 | 159 | # Non-emitter messages are far more frequent, so if it is one, return earlier. 160 | if (not message.channel.startswith("emitter")): 161 | self._invoke_trie_handlers(self._handler_trie_message, self._handler_message, message) 162 | 163 | if self._handler_keygen and message.channel.startswith("emitter/keygen"): 164 | # This is a keygen message. 165 | self._handler_keygen(message.as_object()) 166 | 167 | elif self._handler_keyban and message.channel.startswith("emitter/keyban"): 168 | # This is a keyban message. 169 | self._handler_keyban(message.as_object()) 170 | 171 | elif message.channel.startswith("emitter/presence"): 172 | # This is a presence message. 173 | #self._invoke_trie_handlers(self._handler_trie_presence, self._handler_presence, message) 174 | messageContent = message.as_object() 175 | handlers = self._handler_trie_presence.lookup(messageContent["channel"]) 176 | if len(handlers) == 0 and self._handler_presence: 177 | self._handler_presence(messageContent) 178 | for h in handlers: 179 | h(messageContent) 180 | 181 | elif self._handler_error and message.channel.startswith("emitter/error"): 182 | # This is an error message. 183 | self._handler_error(message.as_object()) 184 | 185 | elif self._handler_me and message.channel.startswith("emitter/me"): 186 | # This is a "me" message, giving information about the connection. 187 | self._handler_me(message.as_object()) 188 | 189 | def connect(self, host="api.emitter.io", port=443, secure=True, keepalive=30, username=None): 190 | """ 191 | * Connects to an Emitter server. 192 | """ 193 | formatted_host = re.sub(r"/.*?:\/\//g", "", host) 194 | self._mqtt = mqtt.Client() 195 | 196 | if secure: 197 | ssl_ctx = ssl.create_default_context() 198 | self._mqtt.tls_set_context(ssl_ctx) 199 | 200 | if username is not None: 201 | self._mqtt.username_pw_set(username) 202 | 203 | self._mqtt.on_connect = self._on_connect 204 | self._mqtt.on_disconnect = self._on_disconnect 205 | self._mqtt.on_message = self._on_message 206 | 207 | self._mqtt.connect(host=formatted_host, port=port, keepalive=keepalive) 208 | 209 | def publish(self, key, channel, message, options={}): 210 | """ 211 | * Publishes a message to a channel. 212 | """ 213 | topic = self._format_channel(key, channel, options) 214 | qos, retain = Client._get_header(options) 215 | 216 | self._mqtt.publish(topic, message, qos=qos, retain=retain) 217 | 218 | def subscribe(self, key, channel, optional_handler=None, options={}): 219 | """ 220 | * Subscribes to a particual share group. 221 | """ 222 | if optional_handler is not None: 223 | self._handler_trie_message.insert(channel, optional_handler) 224 | 225 | topic = Client._format_channel(key, channel, options) 226 | self._mqtt.subscribe(topic) 227 | 228 | def subscribe_with_group(self, key, channel, share_group, optional_handler=None, options={}): 229 | """ 230 | * Subscribes to a particual share group. 231 | """ 232 | if optional_handler is not None: 233 | self._handler_trie_message.insert(channel, optional_handler) 234 | 235 | topic = Client._format_channel_share(key, channel, share_group, options) 236 | self._mqtt.subscribe(topic) 237 | 238 | def unsubscribe(self, key, channel): 239 | """ 240 | * Unsubscribes from a particular channel. 241 | """ 242 | self._handler_trie_message.delete(channel) 243 | topic = self._format_channel(key, channel) 244 | self._mqtt.unsubscribe(topic) 245 | 246 | def disconnect(self): 247 | """ 248 | * Disconnects from the connected Emitter server. 249 | """ 250 | self._mqtt.disconnect() 251 | 252 | def presence(self, key, channel, status=False, changes=False, optional_handler=None): 253 | """ 254 | * Sends a presence request to the server. 255 | """ 256 | if optional_handler is not None: 257 | self._handler_trie_presence.insert(channel, optional_handler) 258 | 259 | request = {"key": key, "channel": channel, "status": status, "changes": changes} 260 | # Publish the request. 261 | self._mqtt.publish("emitter/presence/", json.dumps(request)) 262 | 263 | def keyban(self, master_key, target_key, ban): 264 | """ 265 | * Sends a ban or unban request for a target key using a master key. 266 | """ 267 | request = {"secret": master_key, "target": target_key, "banned": ban} 268 | # Publish the request. 269 | self._mqtt.publish("emitter/keyban/", json.dumps(request)) 270 | 271 | def keygen(self, key, channel, permissions, ttl=0): 272 | """ 273 | * Sends a key generation request to the server. 274 | """ 275 | request = {"key": key, "channel": channel, "type": permissions, "ttl": ttl} 276 | # Publish the request. 277 | self._mqtt.publish("emitter/keygen/", json.dumps(request)) 278 | 279 | def link(self, key, channel, name, subscribe, options={}): 280 | """ 281 | * Sends a link creation request to the server. 282 | """ 283 | formattedChannel = Client._format_channel_link(channel, options=options) 284 | request = {"key": key, "channel": formattedChannel, "name": name, "subscribe": subscribe} 285 | 286 | # Publish the request. 287 | self._mqtt.publish("emitter/link/", json.dumps(request)) 288 | 289 | def publish_with_link(self, link, message): 290 | """ 291 | * Sends a message through the link. 292 | """ 293 | # Publish the request. 294 | self._mqtt.publish(link, message) 295 | 296 | def me(self): 297 | """ 298 | * Requests information about the connection. 299 | """ 300 | self._mqtt.publish("emitter/me/", "") 301 | 302 | @staticmethod 303 | def _get_header(options): 304 | retain = False 305 | qos = 0 306 | for o in options: 307 | if o == Client.RETAIN: 308 | retain = True 309 | elif o == Client.QOS1: 310 | qos = 1 311 | 312 | return qos, retain 313 | 314 | @staticmethod 315 | def _format_options(options): 316 | formatted = "" 317 | 318 | if options != None and len(options) > 0: 319 | formatted = "?" 320 | 321 | for i, o in enumerate(options): 322 | 323 | if o.startswith("+"): 324 | continue 325 | 326 | formatted += o 327 | 328 | if i < len(options) - 1: 329 | formatted += "&" 330 | 331 | return formatted 332 | 333 | 334 | @staticmethod 335 | def _format_channel(key, channel, options={}): 336 | k = key.strip("/") 337 | c = channel.strip("/") 338 | o = Client._format_options(options) 339 | 340 | formatted = "{key}/{channel}/{options}".format(key=k, channel=c, options=o) 341 | return formatted 342 | 343 | @staticmethod 344 | def _format_channel_link(channel, options={}): 345 | c = channel.strip("/") 346 | o = Client._format_options(options) 347 | 348 | formatted = "{channel}/{options}".format(channel=c, options=o) 349 | return formatted 350 | 351 | @staticmethod 352 | def _format_channel_share(key, channel, share_group, options={}): 353 | k = key.strip("/") 354 | c = channel.strip("/") 355 | s = share_group.strip("/") 356 | o = Client._format_options(options) 357 | 358 | formatted = "{key}/$share/{share}/{channel}/{options}".format(key=k, share=s, channel=c, options=o) 359 | return formatted 360 | 361 | RETAIN = "+r" 362 | QOS0 = "+0" 363 | QOS1 = "+1" 364 | 365 | @staticmethod 366 | def with_ttl(ttl): 367 | return "ttl=" + str(ttl) 368 | 369 | @staticmethod 370 | def without_echo(): 371 | return "me=0" 372 | 373 | @staticmethod 374 | def with_last(last): 375 | return "last=" + str(last) 376 | 377 | @staticmethod 378 | def with_retain(): 379 | return Client.RETAIN 380 | 381 | @staticmethod 382 | def with_at_most_once(): 383 | return Client.QOS0 384 | 385 | @staticmethod 386 | def with_at_least_once(): 387 | return Client.QOS1 388 | 389 | 390 | 391 | class EmitterMessage(object): 392 | """ 393 | * Represents a message received from the Emitter server. 394 | """ 395 | 396 | def __init__(self, message): 397 | """ 398 | * Creates an instance of EmitterMessage. 399 | """ 400 | self.channel = message.topic 401 | self.binary = message.payload 402 | 403 | def as_string(self): 404 | """ 405 | * Returns the payload as a utf-8 string. 406 | """ 407 | return str(self.binary) 408 | 409 | # TODO: rename this one and produce a real object. 410 | def as_object(self): 411 | """ 412 | * Returns the payload as an JSON-deserialized dictionary. 413 | """ 414 | msg = None 415 | try: 416 | msg = json.loads(self.binary) 417 | except json.JSONDecodeError as exception: 418 | logging.exception(exception) 419 | 420 | return msg 421 | 422 | def as_binary(self): 423 | """ 424 | * Returns the payload as a raw binary buffer. 425 | """ 426 | return self.binary 427 | -------------------------------------------------------------------------------- /emitter/emitter_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | try: 3 | from .emitter import Client 4 | except ImportError: 5 | from emitter import Client 6 | 7 | def test_format_channel(): 8 | tests = [ 9 | {"key": "5xZjIQp6GA9fpxso1Kslqnv8d4XVWCha", "channel": "test", "options": None, "expected": "5xZjIQp6GA9fpxso1Kslqnv8d4XVWCha/test/"}, 10 | {"key": "5xZjIQp6GA9fpxso1Kslqnv8d4XVWCha", "channel": "test/", "options": None, "expected": "5xZjIQp6GA9fpxso1Kslqnv8d4XVWCha/test/"}, 11 | {"key": "5xZjIQp6GA9fpxso1Kslqnv8d4XVWCha/", "channel": "test", "options": None, "expected": "5xZjIQp6GA9fpxso1Kslqnv8d4XVWCha/test/"}, 12 | {"key": "5xZjIQp6GA9fpxso1Kslqnv8d4XVWCha/", "channel": "test/", "options": None, "expected": "5xZjIQp6GA9fpxso1Kslqnv8d4XVWCha/test/"}, 13 | # With options. 14 | {"key": "5xZjIQp6GA9fpxso1Kslqnv8d4XVWCha", "channel": "test", "options": {Client.with_ttl(5)}, "expected": "5xZjIQp6GA9fpxso1Kslqnv8d4XVWCha/test/?ttl=5"}, 15 | # The following test won't always work, since the enumeration of the options vary in order. 16 | #{"key": "5xZjIQp6GA9fpxso1Kslqnv8d4XVWCha", "channel": "test", "options": {Client.with_ttl(5), Client.without_echo()}, "expected": "5xZjIQp6GA9fpxso1Kslqnv8d4XVWCha/test/?ttl=5&me=0"} 17 | ] 18 | 19 | for test in tests: 20 | formatted = Client._format_channel(key=test["key"], channel=test["channel"], options=test["options"]) 21 | assert formatted == test["expected"] 22 | 23 | def test_format_channel_link(): 24 | tests = [ 25 | {"channel": "test", "options": {Client.with_ttl(5)}, "expected": "test/?ttl=5"}, 26 | ] 27 | 28 | for test in tests: 29 | formatted = Client._format_channel_link(channel=test["channel"], options=test["options"]) 30 | assert formatted == test["expected"] 31 | 32 | # Todo test shared 33 | -------------------------------------------------------------------------------- /emitter/sample-python2.py: -------------------------------------------------------------------------------- 1 | try: 2 | from .emitter import Client 3 | except ImportError: 4 | from emitter import Client 5 | import Tkinter 6 | import json 7 | 8 | emitter = Client() 9 | 10 | root = Tkinter.Tk() 11 | 12 | channel_key = tkinter.StringVar(root, value="8jMLP9F859oDyqmJ3aV4aqnmFZpxApvb") 13 | #channel_key = tkinter.StringVar(root, value="aghbt67CuPawxQvoBfKZ8MpecpPoz7od")#local 14 | channel = tkinter.StringVar(root, value="test/") 15 | shortcut = tkinter.StringVar(root, value="a0") 16 | text_message = tkinter.StringVar(root, value="Hello World") 17 | share_group = tkinter.StringVar(root, value="sg") 18 | share_group_key = tkinter.StringVar(root, value="b7FEsiGFQoSYA6qyeu1dDodFnO0ypp0f") 19 | #share_group_key = tkinter.StringVar(root, value="Q_dM5ODuhWjaR_LNo886hVjoecvt5pMJ") #local 20 | 21 | def connect(): 22 | #emitter.connect(host="127.0.0.1", port=8080, secure=False) 23 | emitter.connect() 24 | 25 | emitter.on_connect = lambda: result_text.insert("0.0", "Connected\n\n") 26 | emitter.on_disconnect = lambda: result_text.insert("0.0", "Disconnected\n\n") 27 | emitter.on_presence = lambda p: result_text.insert("0.0", "Presence message: '" + p.as_string() + "'\n\n") 28 | emitter.on_message = lambda m: result_text.insert("0.0", "Message received on default handler, destined to " + m.channel + ": " + m.as_string() + "\n\n") 29 | emitter.on_error = lambda e: result_text.insert("0.0", "Error received: " + str(e) + "\n\n") 30 | emitter.on_me = lambda me: result_text.insert("0.0", "Information about Me received: " + str(me) +"\n\n") 31 | emitter.loop_start() 32 | 33 | def disconnect(): 34 | emitter.loop_stop() 35 | emitter.disconnect() 36 | 37 | def subscribe(share_group=None): 38 | str_key = channel_key.get() 39 | str_channel = channel.get() 40 | emitter.subscribe(str_key, 41 | str_channel, 42 | optional_handler=lambda m: result_text.insert("0.0", "Message received on handler for " + str_channel + ": " + m.as_string() + "\n\n")) 43 | result_text.insert("0.0", "Subscribtion to '" + str_channel + "' requested.\n\n") 44 | 45 | def subscribe_share(): 46 | str_key = share_group_key.get() 47 | str_channel = channel.get() 48 | str_share = share_group.get() 49 | emitter.subscribe_with_group(str_key, 50 | str_channel, 51 | optional_handler=lambda m: result_text.insert("0.0", "Message received on handler for " + str_channel + ": " + m.as_string() + "\n\n"), 52 | share_group=str_share) 53 | result_text.insert("0.0", "Subscribtion to '" + str_channel + "' requested.\n\n") 54 | 55 | def unsubscribe(): 56 | str_key = channel_key.get() 57 | str_channel = channel.get() 58 | emitter.unsubscribe(str_key, str_channel) 59 | result_text.insert("0.0", "Unsubscribtion to '" + str_channel + "' requested.\n\n") 60 | 61 | def presence(): 62 | str_key = channel_key.get() 63 | str_channel = channel.get() 64 | emitter.presence(str_key, str_channel, status=True, changes=True) 65 | result_text.insert("0.0", "Presence on '" + str_channel + "' requested.\n\n") 66 | 67 | def message(retain=False): 68 | if retain: 69 | emitter.publish(channel_key.get(), channel.get(), text_message.get(), {Client.with_retain()}) 70 | else: 71 | emitter.publish(channel_key.get(), channel.get(), text_message.get(), {}) 72 | result_text.insert("0.0", "Test message send through '" + channel.get() + "'.\n\n") 73 | 74 | def link(): 75 | str_key = channel_key.get() 76 | str_channel = channel.get() 77 | str_link = shortcut.get() 78 | emitter.link(str_key, str_channel, str_link, True) 79 | 80 | def pub_to_link(): 81 | str_link = shortcut.get() 82 | emitter.publish_with_link(str_link, text_message.get()) 83 | 84 | def me(): 85 | emitter.me() 86 | 87 | # Col 1 88 | Tkinter.Label(root, text="Channel : ").grid(column=1, row=1) 89 | channel_entry = Tkinter.Entry(root, width=40, textvariable=channel) 90 | channel_entry.grid(column=1, row=2) 91 | 92 | Tkinter.Label(root, text="Channel key : ").grid(column=1, row=3) 93 | channel_key_entry = Tkinter.Entry(root, width=40, textvariable=channel_key) 94 | channel_key_entry.grid(column=1, row=4) 95 | 96 | Tkinter.Label(root, text="Shortcut : ").grid(column=1, row=5) 97 | shortcut_entry = Tkinter.Entry(root, width=40, textvariable=shortcut) 98 | shortcut_entry.grid(column=1, row=6) 99 | 100 | Tkinter.Label(root, text="Message : ").grid(column=1, row=7) 101 | message_entry = Tkinter.Entry(root, width=40, textvariable=text_message) 102 | message_entry.grid(column=1, row=8) 103 | 104 | Tkinter.Label(root, text="Share group : ").grid(column=1, row=9) 105 | share_entry = Tkinter.Entry(root, width=40, textvariable=share_group) 106 | share_entry.grid(column=1, row=10) 107 | 108 | Tkinter.Label(root, text="Share group key : ").grid(column=1, row=11) 109 | share_key_entry = Tkinter.Entry(root, width=40, textvariable=share_group_key) 110 | share_key_entry.grid(column=1, row=12) 111 | 112 | 113 | # Col 2 114 | connect_button = Tkinter.Button(root, text="Connect", width=30, command=connect) 115 | connect_button.grid(column=2, row=1) 116 | 117 | disconnect_button = Tkinter.Button(root, text="Disconnect", width=30, command=disconnect) 118 | disconnect_button.grid(column=2, row=2) 119 | 120 | subscribe_button = Tkinter.Button(root, text="Subscribe", width=30, command=subscribe) 121 | subscribe_button.grid(column=2, row=4) 122 | 123 | unsubscribe_button = Tkinter.Button(root, text="Unsubscribe", width=30, command=unsubscribe) 124 | unsubscribe_button.grid(column=2, row=5) 125 | 126 | subscribe_share_button = Tkinter.Button(root, text="Subscribe to share", width=30, command=subscribe_share) 127 | subscribe_share_button.grid(column=2, row=6) 128 | 129 | # Col 3 130 | link_button = Tkinter.Button(root, text="Link to shortcut", width=30, command=link) 131 | link_button.grid(column=3, row=1) 132 | 133 | send_button = Tkinter.Button(root, text="Publish to channel", width=30, command=message) 134 | send_button.grid(column=3, row=4) 135 | 136 | send_button = Tkinter.Button(root, text="Publish to channel with retain", width=30, command=lambda: message(retain=True)) 137 | send_button.grid(column=3, row=5) 138 | 139 | pub_to_link_button = Tkinter.Button(root, text="Publish to link", width=30, command=pub_to_link) 140 | pub_to_link_button.grid(column=3, row=6) 141 | 142 | # Col 4 143 | presence_button = Tkinter.Button(root, text="Presence", width=30, command=presence) 144 | presence_button.grid(column=4, row=1) 145 | 146 | me_button = Tkinter.Button(root, text="Me", width=30, command=me) 147 | me_button.grid(column=4, row=2) 148 | 149 | # Text area 150 | result_text = Tkinter.Text(root, height=30, width=120) 151 | result_text.grid(column=1, row=14, columnspan=4) 152 | 153 | root.mainloop() -------------------------------------------------------------------------------- /emitter/sample-python3.py: -------------------------------------------------------------------------------- 1 | try: 2 | from .emitter import Client 3 | except ImportError: 4 | from emitter import Client 5 | import tkinter 6 | import json 7 | 8 | emitter = Client() 9 | 10 | root = tkinter.Tk() 11 | 12 | channel_key = tkinter.StringVar(root, value="8jMLP9F859oDyqmJ3aV4aqnmFZpxApvb") 13 | #channel_key = tkinter.StringVar(root, value="aghbt67CuPawxQvoBfKZ8MpecpPoz7od")#local 14 | channel = tkinter.StringVar(root, value="test/") 15 | shortcut = tkinter.StringVar(root, value="a0") 16 | text_message = tkinter.StringVar(root, value="Hello World") 17 | share_group = tkinter.StringVar(root, value="sg") 18 | share_group_key = tkinter.StringVar(root, value="b7FEsiGFQoSYA6qyeu1dDodFnO0ypp0f") 19 | #share_group_key = tkinter.StringVar(root, value="Q_dM5ODuhWjaR_LNo886hVjoecvt5pMJ") #local 20 | master_key = tkinter.StringVar(root, value="MEj8QNnzy6pKtE887hpXbD0KyKXi4w4f") 21 | 22 | def connect(): 23 | emitter.connect(host="127.0.0.1", port=8080, secure=False) 24 | #emitter.connect() 25 | 26 | emitter.on_connect = lambda: result_text.insert("0.0", "Connected\n\n") 27 | emitter.on_disconnect = lambda: result_text.insert("0.0", "Disconnected\n\n") 28 | emitter.on_presence = lambda p: result_text.insert("0.0", "Presence message on channel: '" + str(p) + "'\n\n") 29 | emitter.on_message = lambda m: result_text.insert("0.0", "Message received on default handler, destined to " + m.channel + ": " + m.as_string() + "\n\n") 30 | emitter.on_error = lambda e: result_text.insert("0.0", "Error received: " + str(e) + "\n\n") 31 | emitter.on_me = lambda me: result_text.insert("0.0", "Information about Me received: " + str(me) + "\n\n") 32 | emitter.on_keyban = lambda kb: result_text.insert("0.0", "Keyban message received: " + str(kb) + "\n\n") 33 | emitter.loop_start() 34 | 35 | def disconnect(): 36 | emitter.loop_stop() 37 | emitter.disconnect() 38 | 39 | def subscribe(share_group=None): 40 | str_key = channel_key.get() 41 | str_channel = channel.get() 42 | emitter.subscribe(str_key, 43 | str_channel, 44 | optional_handler=lambda m: result_text.insert("0.0", "Message received on handler for " + str_channel + ": " + m.as_string() + "\n\n")) 45 | result_text.insert("0.0", "Subscribtion to '" + str_channel + "' requested.\n\n") 46 | 47 | def subscribe_share(): 48 | str_key = share_group_key.get() 49 | str_channel = channel.get() 50 | str_share = share_group.get() 51 | emitter.subscribe_with_group(str_key, 52 | str_channel, 53 | optional_handler=lambda m: result_text.insert("0.0", "Message received on handler for " + str_channel + ": " + m.as_string() + "\n\n"), 54 | share_group=str_share) 55 | result_text.insert("0.0", "Subscribtion to '" + str_channel + "' requested.\n\n") 56 | 57 | def unsubscribe(): 58 | str_key = channel_key.get() 59 | str_channel = channel.get() 60 | emitter.unsubscribe(str_key, str_channel) 61 | result_text.insert("0.0", "Unsubscribtion to '" + str_channel + "' requested.\n\n") 62 | 63 | def presence(): 64 | str_key = channel_key.get() 65 | str_channel = channel.get() 66 | emitter.presence(str_key, str_channel, status=True, changes=True) #optional_handler=lambda p: result_text.insert("0.0", "Optional handler: '" + str(p) + "'\n\n")) 67 | result_text.insert("0.0", "Presence on '" + str_channel + "' requested.\n\n") 68 | 69 | def message(retain=False): 70 | if retain: 71 | emitter.publish(channel_key.get(), channel.get(), text_message.get(), {Client.with_retain()}) 72 | else: 73 | emitter.publish(channel_key.get(), channel.get(), text_message.get(), {}) 74 | result_text.insert("0.0", "Test message send through '" + channel.get() + "'.\n\n") 75 | 76 | def link(): 77 | str_key = channel_key.get() 78 | str_channel = channel.get() 79 | str_link = shortcut.get() 80 | emitter.link(str_key, str_channel, str_link, True) 81 | 82 | def pub_to_link(): 83 | str_link = shortcut.get() 84 | emitter.publish_with_link(str_link, text_message.get()) 85 | 86 | def me(): 87 | emitter.me() 88 | 89 | def keyban(): 90 | str_target_key = channel_key.get() 91 | str_master_key = master_key.get() 92 | emitter.keyban(str_master_key, str_target_key, True) 93 | 94 | def keyunban(): 95 | str_target_key = channel_key.get() 96 | str_master_key = master_key.get() 97 | emitter.keyban(str_master_key, str_target_key, False) 98 | 99 | # Col 1 100 | tkinter.Label(root, text="Channel : ").grid(column=1, row=1) 101 | channel_entry = tkinter.Entry(root, width=40, textvariable=channel) 102 | channel_entry.grid(column=1, row=2) 103 | 104 | tkinter.Label(root, text="Channel key : ").grid(column=1, row=3) 105 | channel_key_entry = tkinter.Entry(root, width=40, textvariable=channel_key) 106 | channel_key_entry.grid(column=1, row=4) 107 | 108 | tkinter.Label(root, text="Shortcut : ").grid(column=1, row=5) 109 | shortcut_entry = tkinter.Entry(root, width=40, textvariable=shortcut) 110 | shortcut_entry.grid(column=1, row=6) 111 | 112 | tkinter.Label(root, text="Message : ").grid(column=1, row=7) 113 | message_entry = tkinter.Entry(root, width=40, textvariable=text_message) 114 | message_entry.grid(column=1, row=8) 115 | 116 | tkinter.Label(root, text="Share group : ").grid(column=1, row=9) 117 | share_entry = tkinter.Entry(root, width=40, textvariable=share_group) 118 | share_entry.grid(column=1, row=10) 119 | 120 | tkinter.Label(root, text="Share group key : ").grid(column=1, row=11) 121 | share_key_entry = tkinter.Entry(root, width=40, textvariable=share_group_key) 122 | share_key_entry.grid(column=1, row=12) 123 | 124 | tkinter.Label(root, text="Master key : ").grid(column=1, row=13) 125 | share_key_entry = tkinter.Entry(root, width=40, textvariable=master_key) 126 | share_key_entry.grid(column=1, row=14) 127 | 128 | # Col 2 129 | connect_button = tkinter.Button(root, text="Connect", width=30, command=connect) 130 | connect_button.grid(column=2, row=1) 131 | 132 | disconnect_button = tkinter.Button(root, text="Disconnect", width=30, command=disconnect) 133 | disconnect_button.grid(column=2, row=2) 134 | 135 | subscribe_button = tkinter.Button(root, text="Subscribe", width=30, command=subscribe) 136 | subscribe_button.grid(column=2, row=4) 137 | 138 | unsubscribe_button = tkinter.Button(root, text="Unsubscribe", width=30, command=unsubscribe) 139 | unsubscribe_button.grid(column=2, row=5) 140 | 141 | subscribe_share_button = tkinter.Button(root, text="Subscribe to share", width=30, command=subscribe_share) 142 | subscribe_share_button.grid(column=2, row=6) 143 | 144 | # Col 3 145 | link_button = tkinter.Button(root, text="Link to shortcut", width=30, command=link) 146 | link_button.grid(column=3, row=1) 147 | 148 | send_button = tkinter.Button(root, text="Publish to channel", width=30, command=message) 149 | send_button.grid(column=3, row=4) 150 | 151 | send_button = tkinter.Button(root, text="Publish to channel with retain", width=30, command=lambda: message(retain=True)) 152 | send_button.grid(column=3, row=5) 153 | 154 | pub_to_link_button = tkinter.Button(root, text="Publish to link", width=30, command=pub_to_link) 155 | pub_to_link_button.grid(column=3, row=6) 156 | 157 | # Col 4 158 | presence_button = tkinter.Button(root, text="Presence", width=30, command=presence) 159 | presence_button.grid(column=4, row=1) 160 | 161 | me_button = tkinter.Button(root, text="Me", width=30, command=me) 162 | me_button.grid(column=4, row=2) 163 | 164 | keyban_button = tkinter.Button(root, text="Key ban", width=30, command=keyban) 165 | keyban_button.grid(column=4, row=4) 166 | 167 | keyban_button = tkinter.Button(root, text="Key unban", width=30, command=keyunban) 168 | keyban_button.grid(column=4, row=5) 169 | 170 | # Text area 171 | result_text = tkinter.Text(root, height=30, width=120) 172 | result_text.grid(column=1, row=15, columnspan=4) 173 | 174 | root.mainloop() 175 | -------------------------------------------------------------------------------- /emitter/subtrie.py: -------------------------------------------------------------------------------- 1 | class SubTrieNode: 2 | def __init__(self, parent, word, handler): 3 | self.parent = parent 4 | self.word = word 5 | self.children = {} 6 | self.handler = handler 7 | 8 | class SubTrie: 9 | def __init__(self): 10 | self.root = SubTrieNode(None, None, None) 11 | 12 | @staticmethod 13 | def _get_words(topic): 14 | return filter(None, topic.split("/")) 15 | 16 | def insert(self, topic, handler): 17 | cur_node = self.root 18 | for word in self._get_words(topic): 19 | if word not in cur_node.children: 20 | cur_node.children[word] = SubTrieNode(cur_node, word, None) 21 | cur_node = cur_node.children[word] 22 | cur_node.handler = handler 23 | 24 | def _lookup(self, route, children): 25 | handlers = [] 26 | 27 | if len(route) == 0: 28 | return handlers 29 | 30 | if route[0] in children: 31 | if children[route[0]].handler != None: 32 | handlers.append(children[route[0]].handler) 33 | handlers = handlers + self._lookup(route[1:], children[route[0]].children) 34 | 35 | if "+" in children: 36 | if children["+"].handler != None: 37 | handlers.append(children["+"].handler) 38 | handlers = handlers + self._lookup(route[1:], children["+"].children) 39 | 40 | return handlers 41 | 42 | def lookup(self, topic): 43 | route = list(self._get_words(topic)) 44 | return self._lookup(route, self.root.children) 45 | 46 | def delete(self, topic): 47 | cur_node = self.root 48 | for word in self._get_words(topic): 49 | if word not in cur_node.children: 50 | return 51 | cur_node = cur_node.children[word] 52 | 53 | cur_node.handler = None 54 | 55 | while cur_node != self.root and cur_node.handler == None and len(cur_node.children) == 0: 56 | del cur_node.parent.children[cur_node.word] 57 | cur_node = cur_node.parent 58 | 59 | return -------------------------------------------------------------------------------- /emitter/subtrie_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | try: 3 | from .subtrie import SubTrie 4 | except ImportError: 5 | from subtrie import SubTrie 6 | 7 | 8 | def test_lookup_with_wildcard(): 9 | t = SubTrie() 10 | t.insert("a/", lambda: None) 11 | t.insert("a/b/c/", lambda: None) 12 | t.insert("a/+/c/", lambda: None) 13 | t.insert("a/b/c/d/", lambda: None) 14 | t.insert("a/+/c/+/", lambda: None) 15 | t.insert("x/", lambda: None) 16 | t.insert("x/y/", lambda: None) 17 | t.insert("x/+/z/", lambda: None) 18 | 19 | tests = { 20 | "a/": 1, 21 | "a/1/": 1, 22 | "a/2/": 1, 23 | "a/1/2/": 1, 24 | "a/1/2/3/": 1, 25 | "a/x/y/c/": 1, 26 | "a/x/c/": 2, 27 | "a/b/c/": 3, 28 | "a/b/c/d/": 5, 29 | "a/b/c/e/": 4, 30 | "x/y/c/e/": 2} 31 | 32 | for k, v in tests.items(): 33 | results = t.lookup(k) 34 | assert len(results) == v 35 | 36 | def test_replace_handler(): 37 | t = SubTrie() 38 | t.insert("a", lambda: "a1") 39 | t.insert("a", lambda: "a2") 40 | 41 | results = t.lookup("a") 42 | assert len(results) == 1 43 | assert results[0]() == "a2" 44 | 45 | def test_delete_parent(): 46 | t = SubTrie() 47 | t.insert("a", lambda: None) 48 | t.insert("a/b/c", lambda: None) 49 | t.insert("a/+/c", lambda: None) 50 | 51 | t.delete("a") 52 | 53 | results = t.lookup("a") 54 | assert len(results) == 0 55 | 56 | results = t.lookup("a/b") 57 | assert len(results) == 0 58 | 59 | results = t.lookup("a/b/c") 60 | assert len(results) == 2 61 | 62 | def test_delete_child(): 63 | t = SubTrie() 64 | t.insert("a", lambda: None) 65 | t.insert("a/b", lambda: None) 66 | 67 | results = t.lookup("a/b") 68 | assert len(results) == 2 69 | 70 | t.delete("a/b") 71 | 72 | results = t.lookup("a/b") 73 | assert len(results) == 1 74 | 75 | def test_delete_inexistent_child(): 76 | t = SubTrie() 77 | t.insert("a", lambda: None) 78 | t.insert("a/b", lambda: None) 79 | 80 | t.delete("c") 81 | 82 | results = t.lookup("a/b") 83 | assert len(results) == 2 84 | 85 | 86 | def test_delete_root(): 87 | t = SubTrie() 88 | t.insert("a", lambda: None) 89 | 90 | t.delete("a") 91 | 92 | results = t.lookup("a") 93 | assert len(results) == 0 -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="emitter-io", 8 | version="2.3.0", 9 | author="Florimond Husquinet", 10 | author_email="florimond@emitter.io", 11 | description="A Python library to interact with the Emitter API.", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://emitter.io", 15 | packages=["emitter"], 16 | classifiers=[ 17 | "Development Status :: 5 - Production/Stable", 18 | "Intended Audience :: Developers", 19 | "Topic :: Communications", 20 | "Topic :: Internet :: WWW/HTTP", 21 | "License :: OSI Approved :: Eclipse Public License 1.0 (EPL-1.0)", 22 | "Programming Language :: Python :: 2", 23 | "Programming Language :: Python :: 3" 24 | ], 25 | keywords="emitter mqtt realtime cloud service", 26 | install_requires=["paho-mqtt"] 27 | ) 28 | --------------------------------------------------------------------------------