├── .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 |
--------------------------------------------------------------------------------