├── .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 | [](https://pypi.org/project/emitter-io) [](https://github.com/emitter-io/python) [](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 | 
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 |
--------------------------------------------------------------------------------