├── .github ├── pull_request_template.md └── workflows │ └── pypi-publish.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE.txt ├── README.md ├── docs └── spacetimedb_client.md ├── examples └── quickstart │ ├── client │ ├── main.py │ └── module_bindings │ │ ├── message.py │ │ ├── send_message_reducer.py │ │ ├── set_name_reducer.py │ │ └── user.py │ └── server │ ├── .cargo │ └── config.toml │ ├── .gitignore │ ├── Cargo.lock │ ├── Cargo.toml │ └── src │ └── lib.rs ├── lazydocs ├── spacetimedb_sdk.spacetimedb_async_client.md └── spacetimedb_sdk.spacetimedb_client.md ├── pyproject.toml └── src ├── gen_lazydocs.py └── spacetimedb_sdk ├── __init__.py ├── _version.py ├── client_cache.py ├── local_config.py ├── spacetime_websocket_client.py ├── spacetimedb_async_client.py └── spacetimedb_client.py /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description of Changes 2 | *Describe what has been changed, any new features or bug fixes* 3 | 4 | ## API 5 | 6 | - [ ] This is an API breaking change to the SDK 7 | 8 | *If the API is breaking, please state below what will break* 9 | 10 | 11 | ## Requires SpacetimeDB PRs 12 | *List any PRs here that are required for this SDK change to work* 13 | -------------------------------------------------------------------------------- /.github/workflows/pypi-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to PyPI 2 | on: 3 | release: 4 | types: [published] 5 | 6 | permissions: 7 | contents: read 8 | 9 | jobs: 10 | deploy: 11 | 12 | runs-on: ubuntu-latest 13 | env: 14 | MY_PACKAGE_VERSION: ${{ github.event.release.tag_name }} 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Set up Python 18 | uses: actions/setup-python@v3 19 | with: 20 | python-version: '3.x' 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install build 25 | - name: Build package 26 | run: python -m build 27 | - name: Publish package 28 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 29 | with: 30 | user: __token__ 31 | password: ${{ secrets.PYPI_API_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # General 2 | __pycache__/ 3 | *.pyc 4 | *.pyo 5 | *.pyd 6 | *.log 7 | 8 | # Virtual environments 9 | venv/ 10 | *.venv/ 11 | 12 | # dist folder 13 | /dist/ 14 | /src/spacetimedb_sdk.egg-info/ -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[python]": { 3 | "editor.defaultFormatter": "ms-python.black-formatter" 4 | }, 5 | "python.formatting.provider": "none" 6 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Notice: Seeking Maintainers 2 | 3 | Due to resource constraints, we are not actively maintaining the SpacetimeDB Python SDK at this time. Official Python SDK support will return in the future when Python modules are also supported. 4 | 5 | For now, help wanted! We believe in the importance of open-source collaboration and want to ensure that SpacetimeDB continues to thrive. Therefore, we are seeking individuals or organizations who are interested in taking over the maintenance and development of the Python SDK. 6 | 7 | If you are passionate about SpacetimeDB and have the time and expertise to contribute, we welcome you to step forward and become a maintainer. Your contributions will be highly valued by the community and will help ensure the longevity and sustainability of this project. 8 | 9 | # SpacetimeDB SDK for Python 10 | 11 | ## Overview 12 | 13 | This repository contains the [Python](https://python.org/) SDK for SpacetimeDB. The SDK allows to interact with the database server and is prepared to work with code generated from a SpacetimeDB backend code. 14 | 15 | ## Documentation 16 | 17 | The Python SDK has a [Quick Start](https://spacetimedb.com/docs/client-languages/python/python-sdk-quickstart-guide) guide and a [Reference](https://spacetimedb.com/docs/client-languages/python/python-sdk-reference). 18 | 19 | ## Installation 20 | 21 | The SDK is available as a [PyPi package](https://pypi.org/project/spacetimedb-sdk/). To install it, run the following command: 22 | 23 | ```bash 24 | pip install spacetimedb-sdk 25 | ``` 26 | 27 | ## Usage 28 | 29 | The Python SpacetimeDB SDK utilizes a client that uses the `asyncio` package to provide an event driven interface. 30 | 31 | ### Connecting to SpacetimeDB 32 | 33 | To connect to SpacetimeDB, you first need to create a `SpacetimeDBAsyncClient` instance. The `SpacetimeDBAsyncClient` constructor takes the `module_bindings` folder that contains the auto-generated files as a parameter. The `module_bindings` folder is generated by the CLI generate command. 34 | 35 | ```python 36 | from spacetimedb_sdk.spacetimedb_async_client import SpacetimeDBAsyncClient 37 | 38 | spacetime_client = SpacetimeDBAsyncClient(module_bindings) 39 | ``` 40 | 41 | To connect to SpacetimeDB, you need to call the `run` method on the `SpacetimeDBAsyncClient` instance. The `run` method takes the following parameters: 42 | 43 | - `auth_token`: The authentication token to use to connect to SpacetimeDB. This token is generated by the backend code and is used to authenticate the client. 44 | - `host_name`: The hostname of the SpacetimeDB server. This hostname should also contain the port number. For example: `http://localhost:3000`. 45 | - `module_address`: The address of the module to connect to. This is the same address that you use to connect to the SpacetimeDB web interface. 46 | - `ssl_enabled`: Whether to use SSL to connect to SpacetimeDB. `True` if connecting to SpacetimeDB Cloud. 47 | - `on_connect`: A callback that is called when the client connects to SpacetimeDB. 48 | - `queries`: A list of queries to subscribe to. The queries are the same queries that you use to subscribe to tables in the SpacetimeDB web interface. 49 | 50 | Example: 51 | 52 | ```python 53 | import asyncio 54 | 55 | asyncio.run( 56 | spacetime_client.run( 57 | local_config.get_string("auth_token"), 58 | "http://localhost:3000", 59 | "chatqs", 60 | on_connect, 61 | ["SELECT * FROM User", "SELECT * FROM Message"], 62 | ) 63 | ) 64 | ``` 65 | 66 | ### Listening to events 67 | 68 | To listen to events, you need to register callbacks on the `SpacetimeDBAsyncClient` instance. The following callbacks are available: 69 | 70 | - `on_subscription_applied`: Called when the client receives the initial data from SpacetimeDB after subscribing to tables. 71 | - scheduled events: You can schedule events to be called at a later time. The `schedule_event` method takes the following parameters: 72 | - `delay`: The delay in seconds before the event is called. 73 | - `callback`: The callback to call when the event is called. 74 | 75 | You can register for row update events on a table. To do this, you need to register callbacks on the auto-generated table class. The following callbacks are available: 76 | 77 | - `register_row_update`: Called when a row is inserted, updated, or deleted from the table. The callback takes the following parameters: 78 | - `row_op`: The operation that was performed on the row. Can be `insert`, `update`, or `delete`. 79 | - `row_old`: The old row value. `None` if the operation is `insert`. 80 | - `row`: The new row value. `None` if the operation is `delete`. 81 | - `reducer_event`: The reducer event that caused the row update. `None` if the row update was not caused by a reducer event. 82 | 83 | Example: 84 | 85 | ```python 86 | def on_message_row_update(row_op, message_old, message, reducer_event): 87 | if reducer_event is not None and row_op == "insert": 88 | print_message(message) 89 | 90 | Message.register_row_update(on_message_row_update) 91 | ``` 92 | 93 | You can register for reducer call updates as well. 94 | 95 | - `register_on_REDUCER`: Called when a reducer call is received from SpacetimeDB. (If a) you are subscribed to the table that the reducer modifies or b) You called the reducer and it failed) 96 | 97 | Example: 98 | 99 | ```python 100 | def on_send_message_reducer(sender, status, message, msg): 101 | if sender == local_identity: 102 | if status == "failed": 103 | print(f"Failed to send message: {message}") 104 | 105 | send_message_reducer.register_on_send_message(on_send_message_reducer) 106 | ``` 107 | 108 | ### Accessing the client cache 109 | 110 | The client cache is a local cache of the data that the client has received from SpacetimeDB. The client cache is automatically updated when the client receives updates from SpacetimeDB. 111 | 112 | When you run the CLI generate command, SpacetimeDB will automatically generate a class for each table in your database. 113 | 114 | - `filter_by_COLUMN`: Filters the table by the specified column value. 115 | - `iter`: Returns an iterator over the table. 116 | 117 | Example: 118 | 119 | ```python 120 | from module_bindings.user import User 121 | 122 | my_user = User.filter_by_identity(local_identity) 123 | 124 | for user in User.iter(): 125 | print(user.name) 126 | ``` 127 | 128 | ### Calling Reducers 129 | 130 | To call a reducer, you need to call the autogenerated method in the auto-generated reducer file. 131 | 132 | Example: 133 | 134 | ```python 135 | import module_bindings.send_message_reducer as send_message_reducer 136 | 137 | send_message_reducer.send_message("Hello World!") 138 | ``` 139 | -------------------------------------------------------------------------------- /docs/spacetimedb_client.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | # module `spacetimedb_client` 6 | 7 | 8 | 9 | 10 | 11 | 12 | --- 13 | 14 | 15 | 16 | ## class `DbEvent` 17 | Represents a database event. 18 | 19 | 20 | 21 | **Attributes:** 22 | 23 | - `table_name` (str): The name of the table associated with the event. 24 | - `row_pk` (str): The primary key of the affected row. 25 | - `row_op` (str): The operation performed on the row (e.g., "insert", "update", "delete"). 26 | - `decoded_value` (Any, optional): The decoded value of the affected row. Defaults to None. 27 | 28 | 29 | 30 | ### method `DbEvent.__init__` 31 | 32 | ```python 33 | __init__(table_name, row_pk, row_op, decoded_value=None) 34 | ``` 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | --- 45 | 46 | 47 | 48 | ## class `TransactionUpdateMessage` 49 | Represents a transaction update message. Used in on_event callbacks. 50 | 51 | For more details, see `spacetimedb_client.SpacetimeDBClient.register_on_event` 52 | 53 | 54 | 55 | **Attributes:** 56 | 57 | - `caller_identity` (str): The identity of the caller. 58 | - `status` (str): The status of the transaction. 59 | - `message` (str): A message associated with the transaction update. 60 | - `reducer` (str): The reducer used for the transaction. 61 | - `args` (dict): Additional arguments for the transaction. 62 | - `events` (List[DbEvent]): List of DBEvents that were committed. 63 | 64 | 65 | 66 | ### method `TransactionUpdateMessage.__init__` 67 | 68 | ```python 69 | __init__( 70 | caller_identity: str, 71 | status: str, 72 | message: str, 73 | reducer: str, 74 | args: Dict 75 | ) 76 | ``` 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | --- 86 | 87 | 88 | 89 | ### method `TransactionUpdateMessage.append_event` 90 | 91 | ```python 92 | append_event(event) 93 | ``` 94 | 95 | 96 | 97 | 98 | 99 | 100 | --- 101 | 102 | 103 | 104 | ## class `SpacetimeDBClient` 105 | The SpacetimeDBClient class is the primary interface for communication with the SpacetimeDB Module in the SDK, facilitating interaction with the database. 106 | 107 | 108 | 109 | ### method `SpacetimeDBClient.close` 110 | 111 | ```python 112 | close() 113 | ``` 114 | 115 | Close the WebSocket connection. 116 | 117 | This function closes the WebSocket connection to the SpaceTimeDB module. 118 | 119 | 120 | 121 | **Notes:** 122 | 123 | > - This needs to be called when exiting the application to terminate the websocket threads. 124 | > 125 | 126 | **Example:** 127 | SpacetimeDBClient.instance.close() 128 | 129 | --- 130 | 131 | 132 | 133 | ### classmethod `SpacetimeDBClient.init` 134 | 135 | ```python 136 | init( 137 | host: str, 138 | address_or_name: str, 139 | ssl_enabled: bool, 140 | autogen_package: module, 141 | on_connect: Callable[[], NoneType] = None, 142 | on_disconnect: Callable[[str], NoneType] = None, 143 | on_error: Callable[[str], NoneType] = None 144 | ) 145 | ``` 146 | 147 | Create a network manager instance. 148 | 149 | 150 | 151 | **Args:** 152 | 153 | - `host` (str): Hostname:port for SpacetimeDB connection 154 | - `address_or_name` (str): The name or address of the database to connect to 155 | - `autogen_package` (ModuleType): Python package where SpaceTimeDB module generated files are located. 156 | - `on_connect` (Callable[[], None], optional): Optional callback called when a connection is made to the SpaceTimeDB module. 157 | - `on_disconnect` (Callable[[str], None], optional): Optional callback called when the Python client is disconnected from the SpaceTimeDB module. The argument is the close message. 158 | - `on_error` (Callable[[str], None], optional): Optional callback called when the Python client connection encounters an error. The argument is the error message. 159 | 160 | 161 | 162 | **Example:** 163 | SpacetimeDBClient.init(autogen, on_connect=self.on_connect) 164 | 165 | --- 166 | 167 | 168 | 169 | ### method `SpacetimeDBClient.register_on_event` 170 | 171 | ```python 172 | register_on_event( 173 | callback: Callable[[spacetimedb_client.TransactionUpdateMessage], NoneType] 174 | ) 175 | ``` 176 | 177 | Register a callback function to handle transaction update events. 178 | 179 | This function registers a callback function that will be called when a reducer modifies a table matching any of the subscribed queries or if a reducer called by this Python client encounters a failure. 180 | 181 | 182 | 183 | **Args:** 184 | callback (Callable[[TransactionUpdateMessage], None]): A callback function that takes a single argument of type `TransactionUpdateMessage`. This function will be invoked with a `TransactionUpdateMessage` instance containing information about the transaction update event. 185 | 186 | 187 | 188 | **Example:** 189 | def handle_event(transaction_update): # Code to handle the transaction update event 190 | 191 | SpacetimeDBClient.instance.register_on_event(handle_event) 192 | 193 | --- 194 | 195 | 196 | 197 | ### method `SpacetimeDBClient.register_on_transaction` 198 | 199 | ```python 200 | register_on_transaction(callback: Callable[[], NoneType]) 201 | ``` 202 | 203 | Register a callback function to be executed on each transaction. 204 | 205 | 206 | 207 | **Args:** 208 | 209 | - `callback` (Callable[[], None]): A callback function that will be invoked on each transaction. The callback function should not accept any arguments and should not return any value. 210 | 211 | 212 | 213 | **Example:** 214 | def transaction_callback(): # Code to be executed on each transaction 215 | 216 | SpacetimeDBClient.instance.register_on_transaction(transaction_callback) 217 | 218 | --- 219 | 220 | 221 | 222 | ### method `SpacetimeDBClient.subscribe` 223 | 224 | ```python 225 | subscribe(queries: List[str]) 226 | ``` 227 | 228 | Subscribe to receive data and transaction updates for the provided queries. 229 | 230 | This function sends a subscription request to the SpaceTimeDB module, indicating that the client wants to receive data and transaction updates related to the specified queries. 231 | 232 | 233 | 234 | **Args:** 235 | 236 | - `queries` (List[str]): A list of queries to subscribe to. Each query is a string representing an sql formatted query statement. 237 | 238 | 239 | 240 | **Example:** 241 | queries = ["SELECT * FROM table1", "SELECT * FROM table2 WHERE col2 = 0"] SpacetimeDBClient.instance.subscribe(queries) 242 | 243 | --- 244 | 245 | 246 | 247 | ### method `SpacetimeDBClient.update` 248 | 249 | ```python 250 | update() 251 | ``` 252 | 253 | Process incoming messages from the SpaceTimeDB module. 254 | 255 | This function needs to be called on a regular frequency to handle and process incoming messages from the SpaceTimeDB module. It ensures that the client stays synchronized with the module and processes any updates or notifications received. 256 | 257 | 258 | 259 | **Notes:** 260 | 261 | > - Calling this function allows the client to receive and handle new messages from the module. - It's important to ensure that this function is called frequently enough to maintain synchronization with the module and avoid missing important updates. 262 | > 263 | 264 | **Example:** 265 | SpacetimeDBClient.init(autogen, on_connect=self.on_connect) while True: SpacetimeDBClient.instance.update() # Call the update function in a loop to process incoming messages # Additional logic or code can be added here 266 | 267 | 268 | 269 | 270 | --- 271 | 272 | _This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ 273 | -------------------------------------------------------------------------------- /examples/quickstart/client/main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from multiprocessing import Queue 3 | import threading 4 | 5 | from spacetimedb_sdk.spacetimedb_async_client import SpacetimeDBAsyncClient 6 | import spacetimedb_sdk.local_config as local_config 7 | 8 | import module_bindings 9 | from module_bindings.user import User 10 | from module_bindings.message import Message 11 | import module_bindings.send_message_reducer as send_message_reducer 12 | import module_bindings.set_name_reducer as set_name_reducer 13 | 14 | input_queue = Queue() 15 | local_identity = None 16 | 17 | 18 | def run_client(spacetime_client): 19 | asyncio.run( 20 | spacetime_client.run( 21 | local_config.get_string("auth_token"), 22 | "localhost:3000", 23 | "chat", 24 | False, 25 | on_connect, 26 | ["SELECT * FROM User", "SELECT * FROM Message"], 27 | ) 28 | ) 29 | 30 | 31 | def input_loop(): 32 | global input_queue 33 | 34 | while True: 35 | user_input = input() 36 | if len(user_input) == 0: 37 | return 38 | elif user_input.startswith("/name "): 39 | input_queue.put(("name", user_input[6:])) 40 | else: 41 | input_queue.put(("message", user_input)) 42 | 43 | 44 | def on_connect(auth_token, identity): 45 | global local_identity 46 | local_identity = identity 47 | 48 | local_config.set_string("auth_token", auth_token) 49 | 50 | 51 | def check_commands(): 52 | global input_queue 53 | 54 | if not input_queue.empty(): 55 | choice = input_queue.get() 56 | if choice[0] == "name": 57 | set_name_reducer.set_name(choice[1]) 58 | else: 59 | send_message_reducer.send_message(choice[1]) 60 | 61 | spacetime_client.schedule_event(0.1, check_commands) 62 | 63 | 64 | def print_messages_in_order(): 65 | all_messages = sorted(Message.iter(), key=lambda x: x.sent) 66 | for entry in all_messages: 67 | print( 68 | f"{user_name_or_identity(User.filter_by_identity(entry.sender))}: {entry.text}" 69 | ) 70 | 71 | 72 | def on_subscription_applied(): 73 | print(f"\nSYSTEM: Connected.") 74 | print_messages_in_order() 75 | 76 | 77 | def on_send_message_reducer(sender_id, sender_address, status, message, msg): 78 | if sender_id == local_identity: 79 | if status == "failed": 80 | print(f"Failed to send message: {message}") 81 | 82 | 83 | def on_set_name_reducer(sender_id, sender_address, status, message, name): 84 | if sender_id == local_identity: 85 | if status == "failed": 86 | print(f"Failed to set name: {message}") 87 | 88 | 89 | def on_message_row_update(row_op, message_old, message, reducer_event): 90 | if reducer_event is not None and row_op == "insert": 91 | print_message(message) 92 | 93 | 94 | def print_message(message): 95 | user = User.filter_by_identity(message.sender) 96 | user_name = "unknown" 97 | if user is not None: 98 | user_name = user_name_or_identity(user) 99 | 100 | print(f"{user_name}: {message.text}") 101 | 102 | 103 | def user_name_or_identity(user): 104 | if user.name: 105 | return user.name 106 | else: 107 | return (str(user.identity))[:8] 108 | 109 | 110 | def on_user_row_update(row_op, user_old, user, reducer_event): 111 | if row_op == "insert": 112 | if user.online: 113 | print(f"User {user_name_or_identity(user)} connected.") 114 | elif row_op == "update": 115 | if user_old.online and not user.online: 116 | print(f"User {user_name_or_identity(user)} disconnected.") 117 | elif not user_old.online and user.online: 118 | print(f"User {user_name_or_identity(user)} connected.") 119 | 120 | if user_old.name != user.name: 121 | print( 122 | f"User {user_name_or_identity(user_old)} renamed to {user_name_or_identity(user)}." 123 | ) 124 | 125 | 126 | def register_callbacks(spacetime_client): 127 | spacetime_client.register_on_subscription_applied(on_subscription_applied) 128 | 129 | User.register_row_update(on_user_row_update) 130 | Message.register_row_update(on_message_row_update) 131 | 132 | set_name_reducer.register_on_set_name(on_set_name_reducer) 133 | send_message_reducer.register_on_send_message(on_send_message_reducer) 134 | 135 | spacetime_client.schedule_event(0.1, check_commands) 136 | 137 | 138 | if __name__ == "__main__": 139 | local_config.init(".spacetimedb-python-quickstart") 140 | 141 | spacetime_client = SpacetimeDBAsyncClient(module_bindings) 142 | 143 | register_callbacks(spacetime_client) 144 | 145 | thread = threading.Thread(target=run_client, args=(spacetime_client,)) 146 | thread.start() 147 | 148 | input_loop() 149 | 150 | spacetime_client.force_close() 151 | thread.join() 152 | -------------------------------------------------------------------------------- /examples/quickstart/client/module_bindings/message.py: -------------------------------------------------------------------------------- 1 | # THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE 2 | # WILL NOT BE SAVED. MODIFY TABLES IN RUST INSTEAD. 3 | 4 | from __future__ import annotations 5 | from typing import List, Iterator, Callable 6 | 7 | from spacetimedb_sdk.spacetimedb_client import SpacetimeDBClient, Identity, Address 8 | from spacetimedb_sdk.spacetimedb_client import ReducerEvent 9 | 10 | class Message: 11 | is_table_class = True 12 | 13 | @classmethod 14 | def register_row_update(cls, callback: Callable[[str,Message,Message,ReducerEvent], None]): 15 | SpacetimeDBClient.instance._register_row_update("Message",callback) 16 | 17 | @classmethod 18 | def iter(cls) -> Iterator[Message]: 19 | return SpacetimeDBClient.instance._get_table_cache("Message").values() 20 | 21 | @classmethod 22 | def filter_by_sender(cls, sender) -> List[Message]: 23 | return [column_value for column_value in SpacetimeDBClient.instance._get_table_cache("Message").values() if column_value.sender == sender] 24 | 25 | @classmethod 26 | def filter_by_sent(cls, sent) -> List[Message]: 27 | return [column_value for column_value in SpacetimeDBClient.instance._get_table_cache("Message").values() if column_value.sent == sent] 28 | 29 | @classmethod 30 | def filter_by_text(cls, text) -> List[Message]: 31 | return [column_value for column_value in SpacetimeDBClient.instance._get_table_cache("Message").values() if column_value.text == text] 32 | 33 | def __init__(self, data: List[object]): 34 | self.data = {} 35 | self.data["sender"] = Identity.from_string(data[0][0]) 36 | self.data["sent"] = int(data[1]) 37 | self.data["text"] = str(data[2]) 38 | 39 | def encode(self) -> List[object]: 40 | return [self.sender, self.sent, self.text] 41 | 42 | def __getattr__(self, name: str): 43 | return self.data.get(name) 44 | -------------------------------------------------------------------------------- /examples/quickstart/client/module_bindings/send_message_reducer.py: -------------------------------------------------------------------------------- 1 | # THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE 2 | # WILL NOT BE SAVED. MODIFY TABLES IN RUST INSTEAD. 3 | 4 | from typing import List, Callable 5 | 6 | from spacetimedb_sdk.spacetimedb_client import SpacetimeDBClient 7 | from spacetimedb_sdk.spacetimedb_client import Identity 8 | from spacetimedb_sdk.spacetimedb_client import Address 9 | 10 | 11 | reducer_name = "send_message" 12 | 13 | def send_message(text: str): 14 | text = text 15 | SpacetimeDBClient.instance._reducer_call("send_message", text) 16 | 17 | def register_on_send_message(callback: Callable[[Identity, Address, str, str, str], None]): 18 | SpacetimeDBClient.instance._register_reducer("send_message", callback) 19 | 20 | def _decode_args(data): 21 | return [str(data[0])] 22 | -------------------------------------------------------------------------------- /examples/quickstart/client/module_bindings/set_name_reducer.py: -------------------------------------------------------------------------------- 1 | # THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE 2 | # WILL NOT BE SAVED. MODIFY TABLES IN RUST INSTEAD. 3 | 4 | from typing import List, Callable 5 | 6 | from spacetimedb_sdk.spacetimedb_client import SpacetimeDBClient 7 | from spacetimedb_sdk.spacetimedb_client import Identity 8 | from spacetimedb_sdk.spacetimedb_client import Address 9 | 10 | 11 | reducer_name = "set_name" 12 | 13 | def set_name(name: str): 14 | name = name 15 | SpacetimeDBClient.instance._reducer_call("set_name", name) 16 | 17 | def register_on_set_name(callback: Callable[[Identity, Address, str, str, str], None]): 18 | SpacetimeDBClient.instance._register_reducer("set_name", callback) 19 | 20 | def _decode_args(data): 21 | return [str(data[0])] 22 | -------------------------------------------------------------------------------- /examples/quickstart/client/module_bindings/user.py: -------------------------------------------------------------------------------- 1 | # THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE 2 | # WILL NOT BE SAVED. MODIFY TABLES IN RUST INSTEAD. 3 | 4 | from __future__ import annotations 5 | from typing import List, Iterator, Callable 6 | 7 | from spacetimedb_sdk.spacetimedb_client import SpacetimeDBClient, Identity, Address 8 | from spacetimedb_sdk.spacetimedb_client import ReducerEvent 9 | 10 | class User: 11 | is_table_class = True 12 | 13 | primary_key = "identity" 14 | 15 | @classmethod 16 | def register_row_update(cls, callback: Callable[[str,User,User,ReducerEvent], None]): 17 | SpacetimeDBClient.instance._register_row_update("User",callback) 18 | 19 | @classmethod 20 | def iter(cls) -> Iterator[User]: 21 | return SpacetimeDBClient.instance._get_table_cache("User").values() 22 | 23 | @classmethod 24 | def filter_by_identity(cls, identity) -> User: 25 | return next(iter([column_value for column_value in SpacetimeDBClient.instance._get_table_cache("User").values() if column_value.identity == identity]), None) 26 | 27 | @classmethod 28 | def filter_by_online(cls, online) -> List[User]: 29 | return [column_value for column_value in SpacetimeDBClient.instance._get_table_cache("User").values() if column_value.online == online] 30 | 31 | def __init__(self, data: List[object]): 32 | self.data = {} 33 | self.data["identity"] = Identity.from_string(data[0][0]) 34 | self.data["name"] = str(data[1]['0']) if '0' in data[1] else None 35 | self.data["online"] = bool(data[2]) 36 | 37 | def encode(self) -> List[object]: 38 | return [self.identity, {'0': [self.name]}, self.online] 39 | 40 | def __getattr__(self, name: str): 41 | return self.data.get(name) 42 | -------------------------------------------------------------------------------- /examples/quickstart/server/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "wasm32-unknown-unknown" 3 | -------------------------------------------------------------------------------- /examples/quickstart/server/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # These are backup files generated by rustfmt 7 | **/*.rs.bk 8 | 9 | # MSVC Windows builds of rustc generate these, which store debugging information 10 | *.pdb 11 | 12 | # Spacetime ignore 13 | /.spacetime 14 | -------------------------------------------------------------------------------- /examples/quickstart/server/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "anyhow" 7 | version = "1.0.72" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" 10 | 11 | [[package]] 12 | name = "approx" 13 | version = "0.3.2" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "f0e60b75072ecd4168020818c0107f2857bb6c4e64252d8d3983f6263b40a5c3" 16 | dependencies = [ 17 | "num-traits", 18 | ] 19 | 20 | [[package]] 21 | name = "arrayvec" 22 | version = "0.7.4" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" 25 | 26 | [[package]] 27 | name = "autocfg" 28 | version = "1.1.0" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 31 | 32 | [[package]] 33 | name = "block-buffer" 34 | version = "0.10.4" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 37 | dependencies = [ 38 | "generic-array", 39 | ] 40 | 41 | [[package]] 42 | name = "cfg-if" 43 | version = "1.0.0" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 46 | 47 | [[package]] 48 | name = "cpufeatures" 49 | version = "0.2.9" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" 52 | dependencies = [ 53 | "libc", 54 | ] 55 | 56 | [[package]] 57 | name = "crypto-common" 58 | version = "0.1.6" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 61 | dependencies = [ 62 | "generic-array", 63 | "typenum", 64 | ] 65 | 66 | [[package]] 67 | name = "decorum" 68 | version = "0.3.1" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "281759d3c8a14f5c3f0c49363be56810fcd7f910422f97f2db850c2920fde5cf" 71 | dependencies = [ 72 | "approx", 73 | "num-traits", 74 | ] 75 | 76 | [[package]] 77 | name = "digest" 78 | version = "0.10.7" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 81 | dependencies = [ 82 | "block-buffer", 83 | "crypto-common", 84 | ] 85 | 86 | [[package]] 87 | name = "either" 88 | version = "1.9.0" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" 91 | 92 | [[package]] 93 | name = "enum-as-inner" 94 | version = "0.6.0" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "5ffccbb6966c05b32ef8fbac435df276c4ae4d3dc55a8cd0eb9745e6c12f546a" 97 | dependencies = [ 98 | "heck", 99 | "proc-macro2", 100 | "quote", 101 | "syn", 102 | ] 103 | 104 | [[package]] 105 | name = "generic-array" 106 | version = "0.14.7" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 109 | dependencies = [ 110 | "typenum", 111 | "version_check", 112 | ] 113 | 114 | [[package]] 115 | name = "getrandom" 116 | version = "0.2.10" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" 119 | dependencies = [ 120 | "cfg-if", 121 | "libc", 122 | "wasi", 123 | ] 124 | 125 | [[package]] 126 | name = "heck" 127 | version = "0.4.1" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 130 | 131 | [[package]] 132 | name = "hex" 133 | version = "0.4.3" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 136 | 137 | [[package]] 138 | name = "humantime" 139 | version = "2.1.0" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 142 | 143 | [[package]] 144 | name = "itertools" 145 | version = "0.10.5" 146 | source = "registry+https://github.com/rust-lang/crates.io-index" 147 | checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" 148 | dependencies = [ 149 | "either", 150 | ] 151 | 152 | [[package]] 153 | name = "keccak" 154 | version = "0.1.4" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "8f6d5ed8676d904364de097082f4e7d240b571b67989ced0240f08b7f966f940" 157 | dependencies = [ 158 | "cpufeatures", 159 | ] 160 | 161 | [[package]] 162 | name = "libc" 163 | version = "0.2.147" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" 166 | 167 | [[package]] 168 | name = "log" 169 | version = "0.4.19" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" 172 | 173 | [[package]] 174 | name = "num-traits" 175 | version = "0.2.16" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" 178 | dependencies = [ 179 | "autocfg", 180 | ] 181 | 182 | [[package]] 183 | name = "once_cell" 184 | version = "1.18.0" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" 187 | 188 | [[package]] 189 | name = "proc-macro2" 190 | version = "1.0.66" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" 193 | dependencies = [ 194 | "unicode-ident", 195 | ] 196 | 197 | [[package]] 198 | name = "quote" 199 | version = "1.0.32" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" 202 | dependencies = [ 203 | "proc-macro2", 204 | ] 205 | 206 | [[package]] 207 | name = "scoped-tls" 208 | version = "1.0.1" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" 211 | 212 | [[package]] 213 | name = "sha3" 214 | version = "0.10.8" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" 217 | dependencies = [ 218 | "digest", 219 | "keccak", 220 | ] 221 | 222 | [[package]] 223 | name = "spacetime-module" 224 | version = "0.1.0" 225 | dependencies = [ 226 | "anyhow", 227 | "log", 228 | "spacetimedb", 229 | ] 230 | 231 | [[package]] 232 | name = "spacetimedb" 233 | version = "0.5.0" 234 | dependencies = [ 235 | "log", 236 | "once_cell", 237 | "scoped-tls", 238 | "spacetimedb-bindings-macro", 239 | "spacetimedb-bindings-sys", 240 | "spacetimedb-lib", 241 | ] 242 | 243 | [[package]] 244 | name = "spacetimedb-bindings-macro" 245 | version = "0.5.0" 246 | dependencies = [ 247 | "humantime", 248 | "proc-macro2", 249 | "quote", 250 | "syn", 251 | ] 252 | 253 | [[package]] 254 | name = "spacetimedb-bindings-sys" 255 | version = "0.5.0" 256 | dependencies = [ 257 | "getrandom", 258 | ] 259 | 260 | [[package]] 261 | name = "spacetimedb-lib" 262 | version = "0.5.0" 263 | dependencies = [ 264 | "anyhow", 265 | "enum-as-inner", 266 | "hex", 267 | "itertools", 268 | "sha3", 269 | "spacetimedb-bindings-macro", 270 | "spacetimedb-sats", 271 | "thiserror", 272 | ] 273 | 274 | [[package]] 275 | name = "spacetimedb-sats" 276 | version = "0.5.0" 277 | dependencies = [ 278 | "arrayvec", 279 | "decorum", 280 | "enum-as-inner", 281 | "itertools", 282 | "spacetimedb-bindings-macro", 283 | "thiserror", 284 | ] 285 | 286 | [[package]] 287 | name = "syn" 288 | version = "2.0.27" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "b60f673f44a8255b9c8c657daf66a596d435f2da81a555b06dc644d080ba45e0" 291 | dependencies = [ 292 | "proc-macro2", 293 | "quote", 294 | "unicode-ident", 295 | ] 296 | 297 | [[package]] 298 | name = "thiserror" 299 | version = "1.0.44" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90" 302 | dependencies = [ 303 | "thiserror-impl", 304 | ] 305 | 306 | [[package]] 307 | name = "thiserror-impl" 308 | version = "1.0.44" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" 311 | dependencies = [ 312 | "proc-macro2", 313 | "quote", 314 | "syn", 315 | ] 316 | 317 | [[package]] 318 | name = "typenum" 319 | version = "1.16.0" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" 322 | 323 | [[package]] 324 | name = "unicode-ident" 325 | version = "1.0.11" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" 328 | 329 | [[package]] 330 | name = "version_check" 331 | version = "0.9.4" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 334 | 335 | [[package]] 336 | name = "wasi" 337 | version = "0.11.0+wasi-snapshot-preview1" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 340 | -------------------------------------------------------------------------------- /examples/quickstart/server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "spacetime-module" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [lib] 9 | crate-type = ["cdylib"] 10 | 11 | [dependencies] 12 | spacetimedb = "0.6" 13 | log = "0.4" 14 | anyhow = "1.0" 15 | -------------------------------------------------------------------------------- /examples/quickstart/server/src/lib.rs: -------------------------------------------------------------------------------- 1 | use spacetimedb::{spacetimedb, ReducerContext, Identity, Timestamp}; 2 | use anyhow::{Result, anyhow}; 3 | 4 | #[spacetimedb(table)] 5 | pub struct User { 6 | #[primarykey] 7 | identity: Identity, 8 | name: Option, 9 | online: bool, 10 | } 11 | 12 | #[spacetimedb(table)] 13 | pub struct Message { 14 | sender: Identity, 15 | sent: Timestamp, 16 | text: String, 17 | } 18 | 19 | #[spacetimedb(init)] 20 | pub fn init() { 21 | // Called when the module is initially published 22 | } 23 | 24 | #[spacetimedb(connect)] 25 | pub fn identity_connected(ctx: ReducerContext) { 26 | if let Some(user) = User::filter_by_identity(&ctx.sender) { 27 | // If this is a returning user, i.e. we already have a `User` with this `Identity`, 28 | // set `online: true`, but leave `name` and `identity` unchanged. 29 | User::update_by_identity(&ctx.sender, User { online: true, ..user }); 30 | } else { 31 | // If this is a new user, create a `User` row for the `Identity`, 32 | // which is online, but hasn't set a name. 33 | User::insert(User { 34 | name: None, 35 | identity: ctx.sender, 36 | online: true, 37 | }).unwrap(); 38 | } 39 | } 40 | 41 | #[spacetimedb(disconnect)] 42 | pub fn identity_disconnected(ctx: ReducerContext) { 43 | if let Some(user) = User::filter_by_identity(&ctx.sender) { 44 | User::update_by_identity(&ctx.sender, User { online: false, ..user }); 45 | } else { 46 | // This branch should be unreachable, 47 | // as it doesn't make sense for a client to disconnect without connecting first. 48 | log::warn!("Disconnect event for unknown user with identity {:?}", ctx.sender); 49 | } 50 | } 51 | 52 | fn validate_name(name: String) -> Result { 53 | if name.is_empty() { 54 | Err(anyhow!("Names must not be empty")) 55 | } else { 56 | Ok(name) 57 | } 58 | } 59 | 60 | #[spacetimedb(reducer)] 61 | pub fn set_name(ctx: ReducerContext, name: String) -> Result<()> { 62 | let name = validate_name(name)?; 63 | if let Some(user) = User::filter_by_identity(&ctx.sender) { 64 | User::update_by_identity(&ctx.sender, User { name: Some(name), ..user }); 65 | Ok(()) 66 | } else { 67 | Err(anyhow!("Cannot set name for unknown user")) 68 | } 69 | } 70 | 71 | fn validate_message(text: String) -> Result { 72 | if text.is_empty() { 73 | Err(anyhow!("Messages must not be empty")) 74 | } else { 75 | Ok(text) 76 | } 77 | } 78 | 79 | #[spacetimedb(reducer)] 80 | pub fn send_message(ctx: ReducerContext, text: String) -> Result<()> { 81 | // Things to consider: 82 | // - Rate-limit messages per-user. 83 | // - Reject messages from unnamed users. 84 | let text = validate_message(text)?; 85 | Message::insert(Message { 86 | sender: ctx.sender, 87 | text, 88 | sent: ctx.timestamp, 89 | }); 90 | Ok(()) 91 | } 92 | -------------------------------------------------------------------------------- /lazydocs/spacetimedb_sdk.spacetimedb_async_client.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | # module `spacetimedb_sdk.spacetimedb_async_client` 6 | SpacetimeDB Python SDK AsyncIO Client 7 | 8 | This module provides a client interface to your SpacetimeDB module using the asyncio library. Essentially, you create your client object, register callbacks, and then start the client using asyncio.run(). 9 | 10 | For details on how to use this module, see the documentation on the SpacetimeDB website and the examples in the examples/asyncio directory. 11 | 12 | 13 | 14 | --- 15 | 16 | 17 | 18 | ## class `SpacetimeDBException` 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | --- 28 | 29 | 30 | 31 | ## class `SpacetimeDBScheduledEvent` 32 | 33 | 34 | 35 | 36 | 37 | 38 | ### method `__init__` 39 | 40 | ```python 41 | __init__(datetime, callback, args) 42 | ``` 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | --- 53 | 54 | 55 | 56 | ## class `SpacetimeDBAsyncClient` 57 | 58 | 59 | 60 | 61 | 62 | 63 | ### method `__init__` 64 | 65 | ```python 66 | __init__(autogen_package) 67 | ``` 68 | 69 | Create a SpacetimeDBAsyncClient object 70 | 71 | 72 | 73 | **Attributes:** 74 | 75 | - `autogen_package `: package folder created by running the generate command from the CLI 76 | 77 | 78 | 79 | 80 | --- 81 | 82 | 83 | 84 | ### method `call_reducer` 85 | 86 | ```python 87 | call_reducer(reducer_name, *reducer_args) 88 | ``` 89 | 90 | Call a reducer on the async loop. This function will not return until the reducer call completes. 91 | 92 | NOTE: DO NOT call this function if you are using the run() function. You should use the auto-generated reducer functions instead. 93 | 94 | 95 | 96 | **Args:** 97 | 98 | - `reducer_name `: name of the reducer to call 99 | - `reducer_args (variable) `: arguments to pass to the reducer 100 | 101 | --- 102 | 103 | 104 | 105 | ### method `close` 106 | 107 | ```python 108 | close() 109 | ``` 110 | 111 | Close the client. This function will not return until the client is closed. 112 | 113 | NOTE: DO NOT call this function if you are using the run() function. It will close for you. 114 | 115 | --- 116 | 117 | 118 | 119 | ### method `connect` 120 | 121 | ```python 122 | connect(auth_token, host, address_or_name, ssl_enabled, subscription_queries=[]) 123 | ``` 124 | 125 | Connect to the server. 126 | 127 | NOTE: DO NOT call this function if you are using the run() function. It will connect for you. 128 | 129 | 130 | 131 | **Args:** 132 | 133 | - `auth_token `: authentication token to use when connecting to the server 134 | - `host `: host name or IP address of the server 135 | - `address_or_name `: address or name of the module to connect to 136 | - `ssl_enabled `: True to use SSL, False to not use SSL 137 | - `subscription_queries `: list of queries to subscribe to 138 | 139 | --- 140 | 141 | 142 | 143 | ### method `force_close` 144 | 145 | ```python 146 | force_close() 147 | ``` 148 | 149 | Signal the client to stop processing events and close the connection to the server. 150 | 151 | This will cause the client to close even if there are scheduled events that have not fired yet. 152 | 153 | --- 154 | 155 | 156 | 157 | ### method `register_on_subscription_applied` 158 | 159 | ```python 160 | register_on_subscription_applied(callback) 161 | ``` 162 | 163 | Register a callback function to be executed when the local cache is updated as a result of a change to the subscription queries. 164 | 165 | 166 | 167 | **Args:** 168 | 169 | - `callback` (Callable[[], None]): A callback function that will be invoked on each subscription update. The callback function should not accept any arguments and should not return any value. 170 | 171 | 172 | 173 | **Example:** 174 | def subscription_callback(): # Code to be executed on each subscription update 175 | 176 | spacetime_client.register_on_subscription_applied(subscription_callback) 177 | 178 | --- 179 | 180 | 181 | 182 | ### method `run` 183 | 184 | ```python 185 | run( 186 | auth_token, 187 | host, 188 | address_or_name, 189 | ssl_enabled, 190 | on_connect, 191 | subscription_queries=[] 192 | ) 193 | ``` 194 | 195 | Run the client. This function will not return until the client is closed. 196 | 197 | 198 | 199 | **Args:** 200 | 201 | - `auth_token `: authentication token to use when connecting to the server 202 | - `host `: host name or IP address of the server 203 | - `address_or_name `: address or name of the module to connect to 204 | - `ssl_enabled `: True to use SSL, False to not use SSL 205 | - `on_connect `: function to call when the client connects to the server 206 | - `subscription_queries `: list of queries to subscribe to 207 | 208 | --- 209 | 210 | 211 | 212 | ### method `schedule_event` 213 | 214 | ```python 215 | schedule_event(delay_secs, callback, *args) 216 | ``` 217 | 218 | Schedule an event to be fired after a delay 219 | 220 | To create a repeating event, call schedule_event() again from within the callback function. 221 | 222 | 223 | 224 | **Args:** 225 | 226 | - `delay_secs `: number of seconds to wait before firing the event 227 | - `callback `: function to call when the event fires 228 | - `args` (variable): arguments to pass to the callback function 229 | 230 | --- 231 | 232 | 233 | 234 | ### method `subscribe` 235 | 236 | ```python 237 | subscribe(queries: List[str]) 238 | ``` 239 | 240 | Subscribe to receive data and transaction updates for the provided queries. 241 | 242 | This function sends a subscription request to the SpacetimeDB module, indicating that the client wants to receive data and transaction updates related to the specified queries. 243 | 244 | 245 | 246 | **Args:** 247 | 248 | - `queries` (List[str]): A list of queries to subscribe to. Each query is a string representing an sql formatted query statement. 249 | 250 | 251 | 252 | **Example:** 253 | queries = ["SELECT * FROM table1", "SELECT * FROM table2 WHERE col2 = 0"] spacetime_client.subscribe(queries) 254 | 255 | 256 | 257 | 258 | --- 259 | 260 | _This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ 261 | -------------------------------------------------------------------------------- /lazydocs/spacetimedb_sdk.spacetimedb_client.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | # module `spacetimedb_sdk.spacetimedb_client` 6 | 7 | 8 | 9 | 10 | 11 | 12 | --- 13 | 14 | 15 | 16 | ## class `Identity` 17 | Represents a user identity. This is a wrapper around the Uint8Array that is recieved from SpacetimeDB. 18 | 19 | 20 | 21 | **Attributes:** 22 | 23 | - `data` (bytes): The identity data. 24 | 25 | 26 | 27 | ### method `__init__` 28 | 29 | ```python 30 | __init__(data) 31 | ``` 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | --- 41 | 42 | 43 | 44 | ### method `from_bytes` 45 | 46 | ```python 47 | from_bytes(data) 48 | ``` 49 | 50 | Returns an Identity object with the data attribute set to the input bytes. 51 | 52 | 53 | 54 | **Args:** 55 | 56 | - `data` (bytes): The input bytes. 57 | 58 | 59 | 60 | **Returns:** 61 | 62 | - `Identity`: The Identity object. 63 | 64 | --- 65 | 66 | 67 | 68 | ### method `from_string` 69 | 70 | ```python 71 | from_string(string) 72 | ``` 73 | 74 | Returns an Identity object with the data attribute set to the byte representation of the input string. 75 | 76 | 77 | 78 | **Args:** 79 | 80 | - `string` (str): The input string. 81 | 82 | 83 | 84 | **Returns:** 85 | 86 | - `Identity`: The Identity object. 87 | 88 | 89 | --- 90 | 91 | 92 | 93 | ## class `DbEvent` 94 | Represents a database event. 95 | 96 | 97 | 98 | **Attributes:** 99 | 100 | - `table_name` (str): The name of the table associated with the event. 101 | - `row_pk` (str): The primary key of the affected row. 102 | - `row_op` (str): The operation performed on the row (e.g., "insert", "update", "delete"). 103 | - `decoded_value` (Any, optional): The decoded value of the affected row. Defaults to None. 104 | 105 | 106 | 107 | ### method `__init__` 108 | 109 | ```python 110 | __init__(table_name, row_pk, row_op, decoded_value=None) 111 | ``` 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | --- 122 | 123 | 124 | 125 | ## class `ReducerEvent` 126 | 127 | 128 | 129 | 130 | 131 | 132 | ### method `__init__` 133 | 134 | ```python 135 | __init__(caller_identity, reducer_name, status, message, args) 136 | ``` 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | --- 147 | 148 | 149 | 150 | ## class `TransactionUpdateMessage` 151 | Represents a transaction update message. Used in on_event callbacks. 152 | 153 | For more details, see `spacetimedb_client.SpacetimeDBClient.register_on_event` 154 | 155 | 156 | 157 | **Attributes:** 158 | 159 | - `caller_identity` (str): The identity of the caller. 160 | - `status` (str): The status of the transaction. 161 | - `message` (str): A message associated with the transaction update. 162 | - `reducer` (str): The reducer used for the transaction. 163 | - `args` (dict): Additional arguments for the transaction. 164 | - `events` (List[DbEvent]): List of DBEvents that were committed. 165 | 166 | 167 | 168 | ### method `__init__` 169 | 170 | ```python 171 | __init__( 172 | caller_identity: Identity, 173 | status: str, 174 | message: str, 175 | reducer_name: str, 176 | args: Dict 177 | ) 178 | ``` 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | --- 188 | 189 | 190 | 191 | ### method `append_event` 192 | 193 | ```python 194 | append_event(table_name, event) 195 | ``` 196 | 197 | 198 | 199 | 200 | 201 | 202 | --- 203 | 204 | 205 | 206 | ## class `SpacetimeDBClient` 207 | The SpacetimeDBClient class is the primary interface for communication with the SpacetimeDB Module in the SDK, facilitating interaction with the database. 208 | 209 | 210 | 211 | ### method `__init__` 212 | 213 | ```python 214 | __init__(autogen_package) 215 | ``` 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | --- 225 | 226 | 227 | 228 | ### method `close` 229 | 230 | ```python 231 | close() 232 | ``` 233 | 234 | Close the WebSocket connection. 235 | 236 | This function closes the WebSocket connection to the SpacetimeDB module. 237 | 238 | 239 | 240 | **Notes:** 241 | 242 | > - This needs to be called when exiting the application to terminate the websocket threads. 243 | > 244 | 245 | **Example:** 246 | SpacetimeDBClient.instance.close() 247 | 248 | --- 249 | 250 | 251 | 252 | ### method `connect` 253 | 254 | ```python 255 | connect( 256 | auth_token, 257 | host, 258 | address_or_name, 259 | ssl_enabled, 260 | on_connect, 261 | on_disconnect, 262 | on_identity, 263 | on_error 264 | ) 265 | ``` 266 | 267 | 268 | 269 | 270 | 271 | --- 272 | 273 | 274 | 275 | ### classmethod `init` 276 | 277 | ```python 278 | init( 279 | auth_token: str, 280 | host: str, 281 | address_or_name: str, 282 | ssl_enabled: bool, 283 | autogen_package: module, 284 | on_connect: Callable[[], NoneType] = None, 285 | on_disconnect: Callable[[str], NoneType] = None, 286 | on_identity: Callable[[str, Identity], NoneType] = None, 287 | on_error: Callable[[str], NoneType] = None 288 | ) 289 | ``` 290 | 291 | Create a network manager instance. 292 | 293 | 294 | 295 | **Args:** 296 | 297 | - `auth_token` (str): This is the token generated by SpacetimeDB that matches the user's identity. If None, token will be generated 298 | - `host` (str): Hostname:port for SpacetimeDB connection 299 | - `address_or_name` (str): The name or address of the database to connect to 300 | - `autogen_package` (ModuleType): Python package where SpacetimeDB module generated files are located. 301 | - `on_connect` (Callable[[], None], optional): Optional callback called when a connection is made to the SpacetimeDB module. 302 | - `on_disconnect` (Callable[[str], None], optional): Optional callback called when the Python client is disconnected from the SpacetimeDB module. The argument is the close message. 303 | - `on_identity` (Callable[[str, bytes], None], optional): Called when the user identity is recieved from SpacetimeDB. First argument is the auth token used to login in future sessions. 304 | - `on_error` (Callable[[str], None], optional): Optional callback called when the Python client connection encounters an error. The argument is the error message. 305 | 306 | 307 | 308 | **Example:** 309 | SpacetimeDBClient.init(autogen, on_connect=self.on_connect) 310 | 311 | --- 312 | 313 | 314 | 315 | ### method `register_on_event` 316 | 317 | ```python 318 | register_on_event(callback: Callable[[TransactionUpdateMessage], NoneType]) 319 | ``` 320 | 321 | Register a callback function to handle transaction update events. 322 | 323 | This function registers a callback function that will be called when a reducer modifies a table matching any of the subscribed queries or if a reducer called by this Python client encounters a failure. 324 | 325 | 326 | 327 | **Args:** 328 | callback (Callable[[TransactionUpdateMessage], None]): A callback function that takes a single argument of type `TransactionUpdateMessage`. This function will be invoked with a `TransactionUpdateMessage` instance containing information about the transaction update event. 329 | 330 | 331 | 332 | **Example:** 333 | def handle_event(transaction_update): # Code to handle the transaction update event 334 | 335 | SpacetimeDBClient.instance.register_on_event(handle_event) 336 | 337 | --- 338 | 339 | 340 | 341 | ### method `register_on_subscription_applied` 342 | 343 | ```python 344 | register_on_subscription_applied(callback: Callable[[], NoneType]) 345 | ``` 346 | 347 | Register a callback function to be executed when the local cache is updated as a result of a change to the subscription queries. 348 | 349 | 350 | 351 | **Args:** 352 | 353 | - `callback` (Callable[[], None]): A callback function that will be invoked on each subscription update. The callback function should not accept any arguments and should not return any value. 354 | 355 | 356 | 357 | **Example:** 358 | def subscription_callback(): # Code to be executed on each subscription update 359 | 360 | SpacetimeDBClient.instance.register_on_subscription_applied(subscription_callback) 361 | 362 | --- 363 | 364 | 365 | 366 | ### method `subscribe` 367 | 368 | ```python 369 | subscribe(queries: List[str]) 370 | ``` 371 | 372 | Subscribe to receive data and transaction updates for the provided queries. 373 | 374 | This function sends a subscription request to the SpacetimeDB module, indicating that the client wants to receive data and transaction updates related to the specified queries. 375 | 376 | 377 | 378 | **Args:** 379 | 380 | - `queries` (List[str]): A list of queries to subscribe to. Each query is a string representing an sql formatted query statement. 381 | 382 | 383 | 384 | **Example:** 385 | queries = ["SELECT * FROM table1", "SELECT * FROM table2 WHERE col2 = 0"] SpacetimeDBClient.instance.subscribe(queries) 386 | 387 | --- 388 | 389 | 390 | 391 | ### method `unregister_on_event` 392 | 393 | ```python 394 | unregister_on_event(callback: Callable[[TransactionUpdateMessage], NoneType]) 395 | ``` 396 | 397 | Unregister a callback function that was previously registered using `register_on_event`. 398 | 399 | 400 | 401 | **Args:** 402 | 403 | - `callback` (Callable[[TransactionUpdateMessage], None]): The callback function to unregister. 404 | 405 | 406 | 407 | **Example:** 408 | SpacetimeDBClient.instance.unregister_on_event(handle_event) 409 | 410 | --- 411 | 412 | 413 | 414 | ### method `unregister_on_subscription_applied` 415 | 416 | ```python 417 | unregister_on_subscription_applied(callback: Callable[[], NoneType]) 418 | ``` 419 | 420 | Unregister a callback function from the subscription update event. 421 | 422 | 423 | 424 | **Args:** 425 | 426 | - `callback` (Callable[[], None]): A callback function that was previously registered with the `register_on_subscription_applied` function. 427 | 428 | 429 | 430 | **Example:** 431 | def subscription_callback(): # Code to be executed on each subscription update 432 | 433 | SpacetimeDBClient.instance.register_on_subscription_applied(subscription_callback) SpacetimeDBClient.instance.unregister_on_subscription_applied(subscription_callback) 434 | 435 | --- 436 | 437 | 438 | 439 | ### method `update` 440 | 441 | ```python 442 | update() 443 | ``` 444 | 445 | Process all pending incoming messages from the SpacetimeDB module. 446 | 447 | NOTE: This function must be called on a regular interval to process incoming messages. 448 | 449 | 450 | 451 | **Example:** 452 | SpacetimeDBClient.init(autogen, on_connect=self.on_connect) while True: SpacetimeDBClient.instance.update() # Call the update function in a loop to process incoming messages # Additional logic or code can be added here 453 | 454 | 455 | 456 | 457 | --- 458 | 459 | _This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ 460 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel", "build"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "spacetimedb_sdk" 7 | authors = [ 8 | { name = "Clockwork Labs", email = "john@clockworklabs.io" }, 9 | ] 10 | 11 | dependencies = [ 12 | "websocket-client", 13 | "configparser", 14 | ] 15 | version = "0.7.0" 16 | readme = "README.md" 17 | 18 | # urls 19 | # Should describe where to find useful info for your project 20 | [project.urls] 21 | homepage = "https://spacetimedb.com" 22 | repository = "https://github.com/clockworklabs/spacetimedb-python-sdk" 23 | -------------------------------------------------------------------------------- /src/gen_lazydocs.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | from lazydocs import generate_docs 3 | 4 | try: 5 | # The parameters of this function correspond to the CLI options 6 | generate_docs( 7 | [ 8 | "spacetimedb_sdk.spacetimedb_async_client", 9 | "spacetimedb_sdk.spacetimedb_client", 10 | ], 11 | output_path="../lazydocs", 12 | remove_package_prefix=True, 13 | ) 14 | except Exception as e: 15 | print(f"Exception occurred: {e}") 16 | traceback.print_exc() 17 | -------------------------------------------------------------------------------- /src/spacetimedb_sdk/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clockworklabs/spacetimedb-python-sdk/e2c0ace513429fba90284100270a69fc420621ab/src/spacetimedb_sdk/__init__.py -------------------------------------------------------------------------------- /src/spacetimedb_sdk/_version.py: -------------------------------------------------------------------------------- 1 | # file generated by setuptools_scm 2 | # don't change, don't track in version control 3 | __version__ = version = '0.5.1.dev0+g3bfa8bf.d20230705' 4 | __version_tuple__ = version_tuple = (0, 5, 1, 'dev0', 'g3bfa8bf.d20230705') 5 | -------------------------------------------------------------------------------- /src/spacetimedb_sdk/client_cache.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import pkgutil 3 | 4 | 5 | def snake_to_camel(snake_case_string): 6 | return snake_case_string.replace("_", " ").title().replace(" ", "") 7 | 8 | 9 | class TableCache: 10 | def __init__(self, table_class): 11 | self.entries = {} 12 | self.table_class = table_class 13 | 14 | def decode(self, value): 15 | return self.table_class(value) 16 | 17 | def set_entry(self, key, value): 18 | self.entries[key] = self.decode(value) 19 | 20 | def set_entry_decoded(self, key, decoded_value): 21 | self.entries[key] = decoded_value 22 | 23 | def delete_entry(self, key): 24 | if key in self.entries: 25 | del self.entries[key] 26 | else: 27 | print(f"[delete_entry] Error, key not found. ({key})") 28 | 29 | def get_entry(self, key): 30 | if key in self.entries: 31 | return self.entries[key] 32 | 33 | def values(self): 34 | return self.entries.values() 35 | 36 | 37 | class ClientCache: 38 | def __init__(self, autogen_package): 39 | self.tables = {} 40 | self.reducer_cache = {} 41 | 42 | for importer, module_name, is_package in pkgutil.iter_modules( 43 | autogen_package.__path__ 44 | ): 45 | if not is_package: 46 | module = importlib.import_module( 47 | f"{autogen_package.__name__}.{module_name}" 48 | ) 49 | 50 | # check if its a reducer 51 | if module_name.endswith("_reducer"): 52 | reducer_name = getattr(module, "reducer_name") 53 | args_class = getattr(module, "_decode_args") 54 | self.reducer_cache[reducer_name] = args_class 55 | else: 56 | # Assuming table class name is the same as the module name 57 | table_class_name = snake_to_camel(module_name) 58 | 59 | if hasattr(module, table_class_name): 60 | table_class = getattr(module, table_class_name) 61 | 62 | # Check for a special property, e.g. 'is_table_class' 63 | if getattr(table_class, "is_table_class", False): 64 | self.tables[table_class_name] = TableCache(table_class) 65 | 66 | def get_table_cache(self, table_name): 67 | return self.tables[table_name] 68 | 69 | def decode(self, table_name, value): 70 | if not table_name in self.tables: 71 | print(f"[decode] Error, table not found. ({table_name})") 72 | return 73 | 74 | return self.tables[table_name].decode(value) 75 | 76 | def set_entry(self, table_name, key, value): 77 | if not table_name in self.tables: 78 | print(f"[set_entry] Error, table not found. ({table_name})") 79 | return 80 | 81 | self.tables[table_name].set_entry(key, value) 82 | 83 | def set_entry_decoded(self, table_name, key, value): 84 | if not table_name in self.tables: 85 | print(f"[set_entry_decoded] Error, table not found. ({table_name})") 86 | return 87 | 88 | self.tables[table_name].set_entry_decoded(key, value) 89 | 90 | def delete_entry(self, table_name, key): 91 | if not table_name in self.tables: 92 | print(f"[delete_entry] Error, table not found. ({table_name})") 93 | return 94 | 95 | self.tables[table_name].delete_entry(key) 96 | 97 | def get_entry(self, table_name, key): 98 | if not table_name in self.tables: 99 | print(f"[get_entry] Error, table not found. ({table_name})") 100 | return 101 | 102 | return self.tables[table_name].get_entry(key) 103 | -------------------------------------------------------------------------------- /src/spacetimedb_sdk/local_config.py: -------------------------------------------------------------------------------- 1 | """ This is an optional component that allows you to store your settings to a config file. 2 | In the conext of SpacetimeDB, this is useful for storing the user's auth token so you can 3 | connect to the same identity each time you run your program. 4 | 5 | Default settings.ini location is USER_DIR/.spacetime_python_sdk/settings.ini 6 | 7 | Example usage: 8 | 9 | import spacetimedb_sdk.local_config as local_config 10 | 11 | # Initialize the config file 12 | local_config.init() 13 | 14 | # Get the auth token if it exists 15 | auth_token = local_config.get_string("auth_token") 16 | 17 | ... use auth_token to connect to SpacetimeDB ... 18 | 19 | # When you get your token from SpacetimeDB, save it to the config file 20 | local_config.set_string("auth_token", auth_token) 21 | 22 | # Next time you run the program, it will load the same token 23 | """ 24 | 25 | import os 26 | import configparser 27 | import sys 28 | 29 | config = None 30 | settings_path = None 31 | 32 | 33 | def init(config_folder=None, config_file=None, config_root=None, config_defaults=None): 34 | """ 35 | Initialize the config file 36 | 37 | Format of config defaults is a dictionary of key/value pairs 38 | 39 | Example: 40 | 41 | import spacetimedb_sdk.local_config as local_config 42 | 43 | config_defaults = { "open_ai_key" : "12345" } 44 | local_config.init(".my_config_folder", config_defaults = config_defaults) 45 | local_config.get_string("open_ai_key") # returns "12345" 46 | 47 | local_config.set_string("auth_token", auth_token) 48 | 49 | Args: 50 | config_folder : folder to store the config file in, Default: .spacetime_python_sdk 51 | config_file : name of the config file, Default: settings.ini 52 | config_root : root folder to store the config file in, Default: USER_DIR 53 | config_defaults : dictionary of default values to store in the config file, Default: None 54 | """ 55 | 56 | global config 57 | global settings_path 58 | 59 | if config_root is None: 60 | config_root = os.path.expanduser("~") 61 | if config_folder is None: 62 | config_folder = ".spacetime_python_sdk" 63 | if config_file is None: 64 | config_file = "settings.ini" 65 | 66 | # this allows you to specify a different settings file from the command line 67 | if "--client" in sys.argv: 68 | client_index = sys.argv.index("--client") 69 | config_file_prefix = config_file.split(".")[0] 70 | config_file_ext = config_file.split(".")[1] 71 | config_file = "{}_{}.{}".format( 72 | config_file_prefix, sys.argv[client_index + 1], config_file_ext 73 | ) 74 | 75 | settings_path = os.path.join(config_root, config_folder, config_file) 76 | 77 | # Create a ConfigParser object and read the settings file (if it exists) 78 | config = configparser.ConfigParser() 79 | if os.path.exists(settings_path): 80 | config.read(settings_path) 81 | else: 82 | # Set some default config values 83 | config["main"] = {} 84 | if config_defaults is not None: 85 | for key, value in config_defaults.items(): 86 | config["main"][key] = value 87 | 88 | 89 | def set_config(config_in): 90 | global config 91 | 92 | for key, value in config_in: 93 | config["main"][key] = value 94 | _save() 95 | 96 | 97 | def get_string(key): 98 | global config 99 | 100 | if key in config["main"]: 101 | return config["main"][key] 102 | return None 103 | 104 | 105 | def set_string(key, value): 106 | global config 107 | 108 | # Update config values at runtime 109 | config["main"][key] = value 110 | _save() 111 | 112 | 113 | def _save(): 114 | global settings_path 115 | global config 116 | 117 | # Write the updated config values to the settings file 118 | os.makedirs(os.path.dirname(settings_path), exist_ok=True) 119 | with open(settings_path, "w") as f: 120 | config.write(f) 121 | -------------------------------------------------------------------------------- /src/spacetimedb_sdk/spacetime_websocket_client.py: -------------------------------------------------------------------------------- 1 | import websocket 2 | import threading 3 | import base64 4 | import binascii 5 | 6 | 7 | class WebSocketClient: 8 | def __init__(self, protocol, on_connect=None, on_close=None, on_error=None, on_message=None, client_address=None): 9 | self._on_connect = on_connect 10 | self._on_close = on_close 11 | self._on_error = on_error 12 | self._on_message = on_message 13 | 14 | self.protocol = protocol 15 | self.ws = None 16 | self.message_thread = None 17 | self.host = None 18 | self.name_or_address = None 19 | self.is_connected = False 20 | self.client_address = client_address 21 | 22 | def connect(self, auth, host, name_or_address, ssl_enabled): 23 | protocol = "wss" if ssl_enabled else "ws" 24 | url = f"{protocol}://{host}/database/subscribe/{name_or_address}" 25 | 26 | if self.client_address is not None: 27 | url += f"?client_address={self.client_address}" 28 | 29 | self.host = host 30 | self.name_or_address = name_or_address 31 | 32 | ws_header = None 33 | if auth: 34 | token_bytes = bytes(f"token:{auth}", "utf-8") 35 | base64_str = base64.b64encode(token_bytes).decode("utf-8") 36 | headers = { 37 | "Authorization": f"Basic {base64_str}", 38 | } 39 | else: 40 | headers = None 41 | 42 | self.ws = websocket.WebSocketApp(url, 43 | on_open=self.on_open, 44 | on_message=self.on_message, 45 | on_error=self.on_error, 46 | on_close=self.on_close, 47 | header=headers, 48 | subprotocols=[self.protocol]) 49 | 50 | self.message_thread = threading.Thread(target=self.ws.run_forever) 51 | self.message_thread.start() 52 | 53 | def decode_hex_string(hex_string): 54 | try: 55 | return binascii.unhexlify(hex_string) 56 | except binascii.Error: 57 | return None 58 | 59 | def send(self, data): 60 | if not self.is_connected: 61 | print("[send] Not connected") 62 | 63 | self.ws.send(data) 64 | 65 | def close(self): 66 | self.ws.close() 67 | 68 | def on_open(self, ws): 69 | self.is_connected = True 70 | if self._on_connect: 71 | self._on_connect() 72 | 73 | def on_message(self, ws, message): 74 | # Process incoming message on a separate thread here 75 | t = threading.Thread(target=self.process_message, args=(message,)) 76 | t.start() 77 | 78 | def process_message(self, message): 79 | if self._on_message: 80 | self._on_message(message) 81 | pass 82 | 83 | def on_error(self, ws, error): 84 | if self._on_error: 85 | self._on_error(error) 86 | 87 | def on_close(self, ws, status_code, close_msg): 88 | if self._on_close: 89 | self._on_close(close_msg) 90 | -------------------------------------------------------------------------------- /src/spacetimedb_sdk/spacetimedb_async_client.py: -------------------------------------------------------------------------------- 1 | """ SpacetimeDB Python SDK AsyncIO Client 2 | 3 | This module provides a client interface to your SpacetimeDB module using the asyncio library. 4 | Essentially, you create your client object, register callbacks, and then start the client 5 | using asyncio.run(). 6 | 7 | For details on how to use this module, see the documentation on the SpacetimeDB website and 8 | the examples in the examples/asyncio directory. 9 | 10 | """ 11 | 12 | from typing import List 13 | import asyncio 14 | from datetime import timedelta 15 | from datetime import datetime 16 | import traceback 17 | 18 | from spacetimedb_sdk.spacetimedb_client import SpacetimeDBClient 19 | 20 | 21 | class SpacetimeDBException(Exception): 22 | pass 23 | 24 | 25 | class SpacetimeDBScheduledEvent: 26 | def __init__(self, datetime, callback, args): 27 | self.fire_time = datetime 28 | self.callback = callback 29 | self.args = args 30 | 31 | 32 | class SpacetimeDBAsyncClient: 33 | request_timeout = 5 34 | 35 | is_connected = False 36 | is_closing = False 37 | identity = None 38 | 39 | def __init__(self, autogen_package): 40 | """ 41 | Create a SpacetimeDBAsyncClient object 42 | 43 | Attributes: 44 | autogen_package : package folder created by running the generate command from the CLI 45 | 46 | """ 47 | self.client = SpacetimeDBClient(autogen_package) 48 | self.prescheduled_events = [] 49 | self.event_queue = None 50 | 51 | def schedule_event(self, delay_secs, callback, *args): 52 | """ 53 | Schedule an event to be fired after a delay 54 | 55 | To create a repeating event, call schedule_event() again from within the callback function. 56 | 57 | Args: 58 | delay_secs : number of seconds to wait before firing the event 59 | callback : function to call when the event fires 60 | args (variable): arguments to pass to the callback function 61 | """ 62 | 63 | # if this is called before we start the async loop, we need to store the event 64 | if self.event_queue is None: 65 | self.prescheduled_events.append((delay_secs, callback, args)) 66 | else: 67 | # convert the delay to a datetime 68 | fire_time = datetime.now() + timedelta(seconds=delay_secs) 69 | scheduled_event = SpacetimeDBScheduledEvent(fire_time, callback, args) 70 | 71 | # create async task 72 | def on_scheduled_event(): 73 | self.event_queue.put_nowait(("scheduled_event", scheduled_event)) 74 | scheduled_event.callback(*scheduled_event.args) 75 | 76 | async def wait_for_delay(): 77 | await asyncio.sleep( 78 | (scheduled_event.fire_time - datetime.now()).total_seconds() 79 | ) 80 | on_scheduled_event() 81 | 82 | asyncio.create_task(wait_for_delay()) 83 | 84 | def register_on_subscription_applied(self, callback): 85 | """ 86 | Register a callback function to be executed when the local cache is updated as a result of a change to the subscription queries. 87 | 88 | Args: 89 | callback (Callable[[], None]): A callback function that will be invoked on each subscription update. 90 | The callback function should not accept any arguments and should not return any value. 91 | 92 | Example: 93 | def subscription_callback(): 94 | # Code to be executed on each subscription update 95 | 96 | spacetime_client.register_on_subscription_applied(subscription_callback) 97 | 98 | """ 99 | 100 | self.client.register_on_subscription_applied(callback) 101 | 102 | def subscribe(self, queries: List[str]): 103 | """ 104 | Subscribe to receive data and transaction updates for the provided queries. 105 | 106 | This function sends a subscription request to the SpacetimeDB module, indicating that the client 107 | wants to receive data and transaction updates related to the specified queries. 108 | 109 | Args: 110 | queries (List[str]): A list of queries to subscribe to. Each query is a string representing 111 | an sql formatted query statement. 112 | 113 | Example: 114 | queries = ["SELECT * FROM table1", "SELECT * FROM table2 WHERE col2 = 0"] 115 | spacetime_client.subscribe(queries) 116 | """ 117 | 118 | self.client.subscribe(queries) 119 | 120 | def force_close(self): 121 | """ 122 | Signal the client to stop processing events and close the connection to the server. 123 | 124 | This will cause the client to close even if there are scheduled events that have not fired yet. 125 | """ 126 | 127 | self.is_closing = True 128 | 129 | # TODO Cancel all scheduled event tasks 130 | 131 | self.event_queue.put_nowait(("force_close", None)) 132 | 133 | async def run( 134 | self, 135 | auth_token, 136 | host, 137 | address_or_name, 138 | ssl_enabled, 139 | on_connect, 140 | subscription_queries=[], 141 | ): 142 | """ 143 | Run the client. This function will not return until the client is closed. 144 | 145 | Args: 146 | auth_token : authentication token to use when connecting to the server 147 | host : host name or IP address of the server 148 | address_or_name : address or name of the module to connect to 149 | ssl_enabled : True to use SSL, False to not use SSL 150 | on_connect : function to call when the client connects to the server 151 | subscription_queries : list of queries to subscribe to 152 | """ 153 | 154 | if not self.event_queue: 155 | self._on_async_loop_start() 156 | 157 | identity_result = await self.connect( 158 | auth_token, host, address_or_name, ssl_enabled, subscription_queries 159 | ) 160 | 161 | if on_connect is not None: 162 | on_connect(identity_result[0], identity_result[1]) 163 | 164 | def on_subscription_applied(): 165 | self.event_queue.put_nowait(("subscription_applied", None)) 166 | 167 | def on_event(event): 168 | self.event_queue.put_nowait(("reducer_transaction", event)) 169 | 170 | self.client.register_on_event(on_event) 171 | self.client.register_on_subscription_applied(on_subscription_applied) 172 | 173 | while not self.is_closing: 174 | event, payload = await self._event() 175 | if event == "disconnected": 176 | if self.is_closing: 177 | return payload 178 | else: 179 | raise payload 180 | elif event == "error": 181 | raise payload 182 | elif event == "force_close": 183 | break 184 | 185 | await self.close() 186 | 187 | async def connect( 188 | self, auth_token, host, address_or_name, ssl_enabled, subscription_queries=[] 189 | ): 190 | """ 191 | Connect to the server. 192 | 193 | NOTE: DO NOT call this function if you are using the run() function. It will connect for you. 194 | 195 | Args: 196 | auth_token : authentication token to use when connecting to the server 197 | host : host name or IP address of the server 198 | address_or_name : address or name of the module to connect to 199 | ssl_enabled : True to use SSL, False to not use SSL 200 | subscription_queries : list of queries to subscribe to 201 | """ 202 | 203 | if not self.event_queue: 204 | self._on_async_loop_start() 205 | 206 | def on_error(error): 207 | self.event_queue.put_nowait(("error", SpacetimeDBException(error))) 208 | 209 | def on_disconnect(close_msg): 210 | if self.is_closing: 211 | self.event_queue.put_nowait(("disconnected", close_msg)) 212 | else: 213 | self.event_queue.put_nowait(("error", SpacetimeDBException(close_msg))) 214 | 215 | def on_identity_received(auth_token, identity, address): 216 | self.identity = identity 217 | self.address = address 218 | self.client.subscribe(subscription_queries) 219 | self.event_queue.put_nowait(("connected", (auth_token, identity))) 220 | 221 | self.client.connect( 222 | auth_token, 223 | host, 224 | address_or_name, 225 | ssl_enabled, 226 | on_connect=None, 227 | on_error=on_error, 228 | on_disconnect=on_disconnect, 229 | on_identity=on_identity_received, 230 | ) 231 | 232 | while True: 233 | event, payload = await self._event() 234 | if event == "error": 235 | raise payload 236 | elif event == "connected": 237 | self.is_connected = True 238 | return payload 239 | 240 | async def call_reducer(self, reducer_name, *reducer_args): 241 | """ 242 | Call a reducer on the async loop. This function will not return until the reducer call completes. 243 | 244 | NOTE: DO NOT call this function if you are using the run() function. You should use the 245 | auto-generated reducer functions instead. 246 | 247 | Args: 248 | reducer_name : name of the reducer to call 249 | reducer_args (variable) : arguments to pass to the reducer 250 | 251 | """ 252 | 253 | def on_reducer_result(event): 254 | if event.reducer == reducer_name and event.caller_identity == self.identity: 255 | self.event_queue.put_nowait(("reducer_result", event)) 256 | 257 | self.client.register_on_event(on_reducer_result) 258 | 259 | timeout_task = asyncio.create_task(self._timeout_task(self.request_timeout)) 260 | 261 | self.client._reducer_call(reducer_name, *reducer_args) 262 | 263 | while True: 264 | event, payload = await self._event() 265 | if event == "reducer_result": 266 | if not timeout_task.done(): 267 | timeout_task.cancel() 268 | return payload 269 | elif event == "timeout": 270 | raise SpacetimeDBException("Reducer call timed out.") 271 | 272 | async def close(self): 273 | """ 274 | Close the client. This function will not return until the client is closed. 275 | 276 | NOTE: DO NOT call this function if you are using the run() function. It will close for you. 277 | """ 278 | self.is_closing = True 279 | 280 | timeout_task = asyncio.create_task(self._timeout_task(self.request_timeout)) 281 | 282 | self.client.close() 283 | 284 | while True: 285 | event, payload = await self._event() 286 | if event == "disconnected": 287 | if not timeout_task.done(): 288 | timeout_task.cancel() 289 | break 290 | elif event == "timeout": 291 | raise SpacetimeDBException("Close time out.") 292 | 293 | def _on_async_loop_start(self): 294 | self.event_queue = asyncio.Queue() 295 | for event in self.prescheduled_events: 296 | self.schedule_event(event[0], event[1], *event[2]) 297 | 298 | async def _timeout_task(self, timeout): 299 | await asyncio.sleep(timeout) 300 | self.event_queue.put_nowait(("timeout",)) 301 | 302 | # TODO: Replace this with a proper async queue 303 | async def _event(self): 304 | update_task = asyncio.create_task(self._periodic_update()) 305 | try: 306 | result = await self.event_queue.get() 307 | update_task.cancel() 308 | return result 309 | except Exception as e: 310 | update_task.cancel() 311 | print(f"Exception: {e}") 312 | raise e 313 | 314 | async def _periodic_update(self): 315 | while True: 316 | try: 317 | self.client.update() 318 | except Exception as e: 319 | print(f"Exception: {e}") 320 | self.event_queue.put_nowait(("error", e)) 321 | return 322 | await asyncio.sleep(0.1) 323 | -------------------------------------------------------------------------------- /src/spacetimedb_sdk/spacetimedb_client.py: -------------------------------------------------------------------------------- 1 | from typing import List, Dict, Callable 2 | from types import ModuleType 3 | 4 | import json 5 | import queue 6 | import random 7 | 8 | from spacetimedb_sdk.spacetime_websocket_client import WebSocketClient 9 | from spacetimedb_sdk.client_cache import ClientCache 10 | 11 | 12 | class Identity: 13 | """ 14 | Represents a user identity. This is a wrapper around the Uint8Array that is recieved from SpacetimeDB. 15 | 16 | Attributes: 17 | data (bytes): The identity data. 18 | """ 19 | 20 | def __init__(self, data): 21 | self.data = bytes(data) # Ensure data is always bytes 22 | 23 | @staticmethod 24 | def from_string(string): 25 | """ 26 | Returns an Identity object with the data attribute set to the byte representation of the input string. 27 | 28 | Args: 29 | string (str): The input string. 30 | 31 | Returns: 32 | Identity: The Identity object. 33 | """ 34 | return Identity(bytes.fromhex(string)) 35 | 36 | @staticmethod 37 | def from_bytes(data): 38 | """ 39 | Returns an Identity object with the data attribute set to the input bytes. 40 | 41 | Args: 42 | data (bytes): The input bytes. 43 | 44 | Returns: 45 | Identity: The Identity object. 46 | """ 47 | return Identity(data) 48 | 49 | # override to_string 50 | def __str__(self): 51 | return self.data.hex() 52 | 53 | # override = operator 54 | def __eq__(self, other): 55 | return isinstance(other, Identity) and self.data == other.data 56 | 57 | def __hash__(self): 58 | return hash(self.data) 59 | 60 | class Address: 61 | """ 62 | Represents a user address. This is a wrapper around the Uint8Array that is recieved from SpacetimeDB. 63 | 64 | Attributes: 65 | data (bytes): The address data. 66 | """ 67 | 68 | def __init__(self, data): 69 | self.data = bytes(data) # Ensure data is always bytes 70 | 71 | @staticmethod 72 | def from_string(string): 73 | """ 74 | Returns an Address object with the data attribute set to the byte representation of the input string. 75 | Returns None if the string is all zeros. 76 | 77 | Args: 78 | string (str): The input string. 79 | 80 | Returns: 81 | Address: The Address object. 82 | """ 83 | address_bytes = bytes.fromhex(string) 84 | if all(byte == 0 for byte in address_bytes): 85 | return None 86 | else: 87 | return Address(address_bytes) 88 | 89 | @staticmethod 90 | def from_bytes(data): 91 | """ 92 | Returns an Address object with the data attribute set to the input bytes. 93 | 94 | Args: 95 | data (bytes): The input bytes. 96 | 97 | Returns: 98 | Address: The Address object. 99 | """ 100 | if all(byte == 0 for byte in address_bytes): 101 | return None 102 | else: 103 | return Address(data) 104 | 105 | @staticmethod 106 | def random(): 107 | """ 108 | Returns a random Address. 109 | """ 110 | return Address(bytes(random.getrandbits(8) for _ in range(16))) 111 | 112 | # override to_string 113 | def __str__(self): 114 | return self.data.hex() 115 | 116 | # override = operator 117 | def __eq__(self, other): 118 | return isinstance(other, Address) and self.data == other.data 119 | 120 | def __hash__(self): 121 | return hash(self.data) 122 | 123 | 124 | class DbEvent: 125 | """ 126 | Represents a database event. 127 | 128 | Attributes: 129 | table_name (str): The name of the table associated with the event. 130 | row_pk (str): The primary key of the affected row. 131 | row_op (str): The operation performed on the row (e.g., "insert", "update", "delete"). 132 | decoded_value (Any, optional): The decoded value of the affected row. Defaults to None. 133 | """ 134 | 135 | def __init__(self, table_name, row_pk, row_op, decoded_value=None): 136 | self.table_name = table_name 137 | self.row_pk = row_pk 138 | self.row_op = row_op 139 | self.decoded_value = decoded_value 140 | 141 | 142 | class _ClientApiMessage: 143 | """ 144 | This class is intended for internal use only and should not be used externally. 145 | """ 146 | 147 | def __init__(self, transaction_type): 148 | self.transaction_type = transaction_type 149 | self.events = {} 150 | 151 | def append_event(self, table_name, event): 152 | self.events.setdefault(table_name, []).append(event) 153 | 154 | 155 | class _IdentityReceivedMessage(_ClientApiMessage): 156 | """ 157 | This class is intended for internal use only and should not be used externally. 158 | """ 159 | 160 | def __init__(self, auth_token, identity, address): 161 | super().__init__("IdentityReceived") 162 | 163 | self.auth_token = auth_token 164 | self.identity = identity 165 | self.address = address 166 | 167 | 168 | class _SubscriptionUpdateMessage(_ClientApiMessage): 169 | """ 170 | This class is intended for internal use only and should not be used externally. 171 | """ 172 | 173 | def __init__(self): 174 | super().__init__("SubscriptionUpdate") 175 | 176 | 177 | class ReducerEvent: 178 | """ 179 | This class contains the information about a reducer event to be passed to row update callbacks. 180 | """ 181 | 182 | def __init__(self, caller_identity, caller_address, reducer_name, status, message, args): 183 | self.caller_identity = caller_identity 184 | self.caller_address = caller_address 185 | self.reducer_name = reducer_name 186 | self.status = status 187 | self.message = message 188 | self.args = args 189 | 190 | 191 | class TransactionUpdateMessage(_ClientApiMessage): 192 | # This docstring appears incorrect 193 | """ 194 | Represents a transaction update message. Used in on_event callbacks. 195 | 196 | For more details, see `spacetimedb_client.SpacetimeDBClient.register_on_event` 197 | 198 | Attributes: 199 | caller_identity (str): The identity of the caller. 200 | status (str): The status of the transaction. 201 | message (str): A message associated with the transaction update. 202 | reducer (str): The reducer used for the transaction. 203 | args (dict): Additional arguments for the transaction. 204 | events (List[DbEvent]): List of DBEvents that were committed. 205 | """ 206 | 207 | def __init__( 208 | self, 209 | caller_identity: Identity, 210 | caller_address: Address, 211 | status: str, 212 | message: str, 213 | reducer_name: str, 214 | args: Dict, 215 | ): 216 | super().__init__("TransactionUpdate") 217 | self.reducer_event = ReducerEvent( 218 | caller_identity, caller_address, reducer_name, status, message, args 219 | ) 220 | 221 | 222 | class SpacetimeDBClient: 223 | """ 224 | The SpacetimeDBClient class is the primary interface for communication with the SpacetimeDB Module in the SDK, facilitating interaction with the database. 225 | """ 226 | 227 | instance = None 228 | client_cache = None 229 | 230 | @classmethod 231 | def init( 232 | cls, 233 | auth_token: str, 234 | host: str, 235 | address_or_name: str, 236 | ssl_enabled: bool, 237 | autogen_package: ModuleType, 238 | on_connect: Callable[[], None] = None, 239 | on_disconnect: Callable[[str], None] = None, 240 | on_identity: Callable[[str, Identity, Address], None] = None, 241 | on_error: Callable[[str], None] = None, 242 | ): 243 | """ 244 | Create a network manager instance. 245 | 246 | Args: 247 | auth_token (str): This is the token generated by SpacetimeDB that matches the user's identity. If None, token will be generated 248 | host (str): Hostname:port for SpacetimeDB connection 249 | address_or_name (str): The name or address of the database to connect to 250 | autogen_package (ModuleType): Python package where SpacetimeDB module generated files are located. 251 | on_connect (Callable[[], None], optional): Optional callback called when a connection is made to the SpacetimeDB module. 252 | on_disconnect (Callable[[str], None], optional): Optional callback called when the Python client is disconnected from the SpacetimeDB module. The argument is the close message. 253 | on_identity (Callable[[str, Identity, Address], None], optional): Called when the user identity is recieved from SpacetimeDB. First argument is the auth token used to login in future sessions. 254 | on_error (Callable[[str], None], optional): Optional callback called when the Python client connection encounters an error. The argument is the error message. 255 | 256 | Example: 257 | SpacetimeDBClient.init(autogen, on_connect=self.on_connect) 258 | """ 259 | client = SpacetimeDBClient(autogen_package) 260 | client.connect( 261 | auth_token, 262 | host, 263 | address_or_name, 264 | ssl_enabled, 265 | on_connect, 266 | on_disconnect, 267 | on_identity, 268 | on_error, 269 | ) 270 | 271 | # Do not call this directly. Use init to instantiate the instance. 272 | def __init__(self, autogen_package): 273 | SpacetimeDBClient.instance = self 274 | 275 | self._row_update_callbacks = {} 276 | self._reducer_callbacks = {} 277 | self._on_subscription_applied = [] 278 | self._on_event = [] 279 | 280 | self.identity = None 281 | self.address = Address.random() 282 | 283 | self.client_cache = ClientCache(autogen_package) 284 | self.message_queue = queue.Queue() 285 | 286 | self.processed_message_queue = queue.Queue() 287 | 288 | def connect( 289 | self, 290 | auth_token, 291 | host, 292 | address_or_name, 293 | ssl_enabled, 294 | on_connect, 295 | on_disconnect, 296 | on_identity, 297 | on_error, 298 | ): 299 | self._on_connect = on_connect 300 | self._on_disconnect = on_disconnect 301 | self._on_identity = on_identity 302 | self._on_error = on_error 303 | 304 | self.wsc = WebSocketClient( 305 | "v1.text.spacetimedb", 306 | on_connect=on_connect, 307 | on_error=on_error, 308 | on_close=on_disconnect, 309 | on_message=self._on_message, 310 | client_address=self.address, 311 | ) 312 | # print("CONNECTING " + host + " " + address_or_name) 313 | self.wsc.connect( 314 | auth_token, 315 | host, 316 | address_or_name, 317 | ssl_enabled, 318 | ) 319 | 320 | def update(self): 321 | """ 322 | Process all pending incoming messages from the SpacetimeDB module. 323 | 324 | NOTE: This function must be called on a regular interval to process incoming messages. 325 | 326 | Example: 327 | SpacetimeDBClient.init(autogen, on_connect=self.on_connect) 328 | while True: 329 | SpacetimeDBClient.instance.update() # Call the update function in a loop to process incoming messages 330 | # Additional logic or code can be added here 331 | """ 332 | self._do_update() 333 | 334 | def close(self): 335 | """ 336 | Close the WebSocket connection. 337 | 338 | This function closes the WebSocket connection to the SpacetimeDB module. 339 | 340 | Notes: 341 | - This needs to be called when exiting the application to terminate the websocket threads. 342 | 343 | Example: 344 | SpacetimeDBClient.instance.close() 345 | """ 346 | 347 | self.wsc.close() 348 | 349 | def subscribe(self, queries: List[str]): 350 | """ 351 | Subscribe to receive data and transaction updates for the provided queries. 352 | 353 | This function sends a subscription request to the SpacetimeDB module, indicating that the client 354 | wants to receive data and transaction updates related to the specified queries. 355 | 356 | Args: 357 | queries (List[str]): A list of queries to subscribe to. Each query is a string representing 358 | an sql formatted query statement. 359 | 360 | Example: 361 | queries = ["SELECT * FROM table1", "SELECT * FROM table2 WHERE col2 = 0"] 362 | SpacetimeDBClient.instance.subscribe(queries) 363 | """ 364 | json_data = json.dumps(queries) 365 | self.wsc.send( 366 | bytes(f'{{"subscribe": {{ "query_strings": {json_data}}}}}', "ascii") 367 | ) 368 | 369 | def register_on_subscription_applied(self, callback: Callable[[], None]): 370 | """ 371 | Register a callback function to be executed when the local cache is updated as a result of a change to the subscription queries. 372 | 373 | Args: 374 | callback (Callable[[], None]): A callback function that will be invoked on each subscription update. 375 | The callback function should not accept any arguments and should not return any value. 376 | 377 | Example: 378 | def subscription_callback(): 379 | # Code to be executed on each subscription update 380 | 381 | SpacetimeDBClient.instance.register_on_subscription_applied(subscription_callback) 382 | """ 383 | if self._on_subscription_applied is None: 384 | self._on_subscription_applied = [] 385 | 386 | self._on_subscription_applied.append(callback) 387 | 388 | def unregister_on_subscription_applied(self, callback: Callable[[], None]): 389 | """ 390 | Unregister a callback function from the subscription update event. 391 | 392 | Args: 393 | callback (Callable[[], None]): A callback function that was previously registered with the `register_on_subscription_applied` function. 394 | 395 | Example: 396 | def subscription_callback(): 397 | # Code to be executed on each subscription update 398 | 399 | SpacetimeDBClient.instance.register_on_subscription_applied(subscription_callback) 400 | SpacetimeDBClient.instance.unregister_on_subscription_applied(subscription_callback) 401 | """ 402 | if self._on_subscription_applied is not None: 403 | self._on_subscription_applied.remove(callback) 404 | 405 | def register_on_event(self, callback: Callable[[TransactionUpdateMessage], None]): 406 | """ 407 | Register a callback function to handle transaction update events. 408 | 409 | This function registers a callback function that will be called when a reducer modifies a table 410 | matching any of the subscribed queries or if a reducer called by this Python client encounters a failure. 411 | 412 | Args: 413 | callback (Callable[[TransactionUpdateMessage], None]): 414 | A callback function that takes a single argument of type `TransactionUpdateMessage`. 415 | This function will be invoked with a `TransactionUpdateMessage` instance containing information 416 | about the transaction update event. 417 | 418 | Example: 419 | def handle_event(transaction_update): 420 | # Code to handle the transaction update event 421 | 422 | SpacetimeDBClient.instance.register_on_event(handle_event) 423 | """ 424 | if self._on_event is None: 425 | self._on_event = [] 426 | 427 | self._on_event.append(callback) 428 | 429 | def unregister_on_event(self, callback: Callable[[TransactionUpdateMessage], None]): 430 | """ 431 | Unregister a callback function that was previously registered using `register_on_event`. 432 | 433 | Args: 434 | callback (Callable[[TransactionUpdateMessage], None]): The callback function to unregister. 435 | 436 | Example: 437 | SpacetimeDBClient.instance.unregister_on_event(handle_event) 438 | """ 439 | if self._on_event is not None: 440 | self._on_event.remove(callback) 441 | 442 | def _get_table_cache(self, table_name: str): 443 | return self.client_cache.get_table_cache(table_name) 444 | 445 | def _register_row_update( 446 | self, 447 | table_name: str, 448 | callback: Callable[[str, object, object, ReducerEvent], None], 449 | ): 450 | if table_name not in self._row_update_callbacks: 451 | self._row_update_callbacks[table_name] = [] 452 | 453 | self._row_update_callbacks[table_name].append(callback) 454 | 455 | def _unregister_row_update( 456 | self, 457 | table_name: str, 458 | callback: Callable[[str, object, object, ReducerEvent], None], 459 | ): 460 | if table_name in self._row_update_callbacks: 461 | self._row_update_callbacks[table_name].remove(callback) 462 | 463 | def _register_reducer(self, reducer_name, callback): 464 | if reducer_name not in self._reducer_callbacks: 465 | self._reducer_callbacks[reducer_name] = [] 466 | 467 | self._reducer_callbacks[reducer_name].append(callback) 468 | 469 | def _unregister_reducer(self, reducer_name, callback): 470 | if reducer_name in self._reducer_callbacks: 471 | self._reducer_callbacks[reducer_name].remove(callback) 472 | 473 | def _reducer_call(self, reducer, *args): 474 | if not self.wsc.is_connected: 475 | print("[reducer_call] Not connected") 476 | 477 | message = { 478 | "fn": reducer, 479 | "args": args, 480 | } 481 | 482 | json_data = json.dumps(message) 483 | # print("_reducer_call(JSON): " + json_data) 484 | self.wsc.send(bytes(f'{{"call": {json_data}}}', "ascii")) 485 | 486 | def _on_message(self, data): 487 | # print("_on_message data: " + data) 488 | message = json.loads(data) 489 | if "IdentityToken" in message: 490 | # is this safe to do in the message thread? 491 | token = message["IdentityToken"]["token"] 492 | identity = Identity.from_string(message["IdentityToken"]["identity"]) 493 | address = Address.from_string(message["IdentityToken"]["address"]) 494 | self.message_queue.put(_IdentityReceivedMessage(token, identity, address)) 495 | elif "SubscriptionUpdate" in message or "TransactionUpdate" in message: 496 | clientapi_message = None 497 | table_updates = None 498 | if "SubscriptionUpdate" in message: 499 | clientapi_message = _SubscriptionUpdateMessage() 500 | table_updates = message["SubscriptionUpdate"]["table_updates"] 501 | if "TransactionUpdate" in message: 502 | spacetime_message = message["TransactionUpdate"] 503 | # DAB Todo: We need reducer codegen to parse the args 504 | clientapi_message = TransactionUpdateMessage( 505 | Identity.from_string(spacetime_message["event"]["caller_identity"]), 506 | Address.from_string(spacetime_message["event"]["caller_address"]), 507 | spacetime_message["event"]["status"], 508 | spacetime_message["event"]["message"], 509 | spacetime_message["event"]["function_call"]["reducer"], 510 | json.loads(spacetime_message["event"]["function_call"]["args"]), 511 | ) 512 | table_updates = message["TransactionUpdate"]["subscription_update"][ 513 | "table_updates" 514 | ] 515 | 516 | for table_update in table_updates: 517 | table_name = table_update["table_name"] 518 | 519 | for table_row_op in table_update["table_row_operations"]: 520 | row_op = table_row_op["op"] 521 | if row_op == "insert": 522 | decoded_value = self.client_cache.decode( 523 | table_name, table_row_op["row"] 524 | ) 525 | clientapi_message.append_event( 526 | table_name, 527 | DbEvent( 528 | table_name, 529 | table_row_op["row_pk"], 530 | row_op, 531 | decoded_value, 532 | ), 533 | ) 534 | if row_op == "delete": 535 | clientapi_message.append_event( 536 | table_name, 537 | DbEvent(table_name, table_row_op["row_pk"], row_op), 538 | ) 539 | 540 | self.message_queue.put(clientapi_message) 541 | 542 | def _do_update(self): 543 | while not self.message_queue.empty(): 544 | next_message = self.message_queue.get() 545 | 546 | if next_message.transaction_type == "IdentityReceived": 547 | self.identity = next_message.identity 548 | self.address = next_message.address 549 | if self._on_identity: 550 | self._on_identity(next_message.auth_token, self.identity, self.address) 551 | else: 552 | # print(f"next_message: {next_message.transaction_type}") 553 | # apply all the event state before calling callbacks 554 | for table_name, table_events in next_message.events.items(): 555 | # first retrieve the old values for all events 556 | for db_event in table_events: 557 | # get the old value for sending callbacks 558 | db_event.old_value = self.client_cache.get_entry( 559 | db_event.table_name, db_event.row_pk 560 | ) 561 | 562 | # if this table has a primary key, find table updates by looking for matching insert/delete events 563 | primary_key = getattr( 564 | self.client_cache.get_table_cache(table_name).table_class, 565 | "primary_key", 566 | None, 567 | ) 568 | # print(f"Primary key: {primary_key}") 569 | if primary_key is not None: 570 | primary_key_row_ops = {} 571 | 572 | for db_event in table_events: 573 | if db_event.row_op == "insert": 574 | # NOTE: we have to do look up in actual data dict because primary_key is a property of the table class 575 | primary_key_value = db_event.decoded_value.data[ 576 | primary_key 577 | ] 578 | else: 579 | primary_key_value = db_event.old_value.data[primary_key] 580 | 581 | if primary_key_value in primary_key_row_ops: 582 | other_db_event = primary_key_row_ops[primary_key_value] 583 | if ( 584 | db_event.row_op == "insert" 585 | and other_db_event.row_op == "delete" 586 | ): 587 | # this is a row update so we need to replace the insert 588 | db_event.row_op = "update" 589 | db_event.old_pk = other_db_event.row_pk 590 | db_event.old_value = other_db_event.old_value 591 | primary_key_row_ops[primary_key_value] = db_event 592 | elif ( 593 | db_event.row_op == "delete" 594 | and other_db_event.row_op == "insert" 595 | ): 596 | # the insert was the row update so just upgrade it to update 597 | primary_key_row_ops[ 598 | primary_key_value 599 | ].row_op = "update" 600 | primary_key_row_ops[ 601 | primary_key_value 602 | ].old_pk = db_event.row_pk 603 | primary_key_row_ops[ 604 | primary_key_value 605 | ].old_value = db_event.old_value 606 | else: 607 | print( 608 | f"Error: duplicate primary key {table_name}:{primary_key_value}" 609 | ) 610 | else: 611 | primary_key_row_ops[primary_key_value] = db_event 612 | 613 | table_events = primary_key_row_ops.values() 614 | next_message.events[table_name] = table_events 615 | 616 | # now we can apply the events to the cache 617 | for db_event in table_events: 618 | # print(f"db_event: {db_event.row_op} {table_name}") 619 | if db_event.row_op == "insert" or db_event.row_op == "update": 620 | # in the case of updates we need to delete the old entry 621 | if db_event.row_op == "update": 622 | self.client_cache.delete_entry( 623 | db_event.table_name, db_event.old_pk 624 | ) 625 | self.client_cache.set_entry_decoded( 626 | db_event.table_name, 627 | db_event.row_pk, 628 | db_event.decoded_value, 629 | ) 630 | elif db_event.row_op == "delete": 631 | self.client_cache.delete_entry( 632 | db_event.table_name, db_event.row_pk 633 | ) 634 | 635 | # now that we have applied the state we can call the callbacks 636 | for table_events in next_message.events.values(): 637 | for db_event in table_events: 638 | # call row update callback 639 | if db_event.table_name in self._row_update_callbacks: 640 | reducer_event = ( 641 | next_message.reducer_event 642 | if next_message.transaction_type == "TransactionUpdate" 643 | else None 644 | ) 645 | for row_update_callback in self._row_update_callbacks[ 646 | db_event.table_name 647 | ]: 648 | row_update_callback( 649 | db_event.row_op, 650 | db_event.old_value, 651 | db_event.decoded_value, 652 | reducer_event, 653 | ) 654 | 655 | if next_message.transaction_type == "SubscriptionUpdate": 656 | # call ontransaction callback 657 | for on_subscription_applied in self._on_subscription_applied: 658 | on_subscription_applied() 659 | 660 | if next_message.transaction_type == "TransactionUpdate": 661 | # call on event callback 662 | for event_callback in self._on_event: 663 | event_callback(next_message) 664 | 665 | # call reducer callback 666 | reducer_event = next_message.reducer_event 667 | if reducer_event.reducer_name in self._reducer_callbacks: 668 | args = [] 669 | decode_func = self.client_cache.reducer_cache[ 670 | reducer_event.reducer_name 671 | ] 672 | 673 | args = decode_func(reducer_event.args) 674 | 675 | for reducer_callback in self._reducer_callbacks[ 676 | reducer_event.reducer_name 677 | ]: 678 | reducer_callback( 679 | reducer_event.caller_identity, 680 | reducer_event.caller_address, 681 | reducer_event.status, 682 | reducer_event.message, 683 | *args, 684 | ) 685 | --------------------------------------------------------------------------------