soulfire 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. soulfire-0.1.1/.gitignore +61 -0
  2. soulfire-0.1.1/PKG-INFO +256 -0
  3. soulfire-0.1.1/README.md +237 -0
  4. soulfire-0.1.1/pyproject.toml +51 -0
  5. soulfire-0.1.1/src/soulfire/__init__.py +38 -0
  6. soulfire-0.1.1/src/soulfire/_auth.py +50 -0
  7. soulfire-0.1.1/src/soulfire/_install.py +514 -0
  8. soulfire-0.1.1/src/soulfire/api_docs_pb2.py +40 -0
  9. soulfire-0.1.1/src/soulfire/api_docs_pb2.pyi +38 -0
  10. soulfire-0.1.1/src/soulfire/automation_connect.py +1700 -0
  11. soulfire-0.1.1/src/soulfire/automation_pb2.py +597 -0
  12. soulfire-0.1.1/src/soulfire/automation_pb2.pyi +758 -0
  13. soulfire-0.1.1/src/soulfire/behaviors.py +171 -0
  14. soulfire-0.1.1/src/soulfire/bot.py +1197 -0
  15. soulfire-0.1.1/src/soulfire/bot_connect.py +1570 -0
  16. soulfire-0.1.1/src/soulfire/bot_live_connect.py +1310 -0
  17. soulfire-0.1.1/src/soulfire/bot_live_pb2.py +410 -0
  18. soulfire-0.1.1/src/soulfire/bot_live_pb2.pyi +681 -0
  19. soulfire-0.1.1/src/soulfire/bot_pb2.py +651 -0
  20. soulfire-0.1.1/src/soulfire/bot_pb2.pyi +1092 -0
  21. soulfire-0.1.1/src/soulfire/client.py +1163 -0
  22. soulfire-0.1.1/src/soulfire/client_connect.py +530 -0
  23. soulfire-0.1.1/src/soulfire/client_pb2.py +121 -0
  24. soulfire-0.1.1/src/soulfire/client_pb2.pyi +107 -0
  25. soulfire-0.1.1/src/soulfire/command_connect.py +270 -0
  26. soulfire-0.1.1/src/soulfire/command_pb2.py +84 -0
  27. soulfire-0.1.1/src/soulfire/command_pb2.pyi +75 -0
  28. soulfire-0.1.1/src/soulfire/common_pb2.py +266 -0
  29. soulfire-0.1.1/src/soulfire/common_pb2.pyi +448 -0
  30. soulfire-0.1.1/src/soulfire/download_connect.py +205 -0
  31. soulfire-0.1.1/src/soulfire/download_pb2.py +63 -0
  32. soulfire-0.1.1/src/soulfire/download_pb2.pyi +41 -0
  33. soulfire-0.1.1/src/soulfire/instance_connect.py +1700 -0
  34. soulfire-0.1.1/src/soulfire/instance_live_connect.py +205 -0
  35. soulfire-0.1.1/src/soulfire/instance_live_pb2.py +54 -0
  36. soulfire-0.1.1/src/soulfire/instance_live_pb2.pyi +37 -0
  37. soulfire-0.1.1/src/soulfire/instance_pb2.py +360 -0
  38. soulfire-0.1.1/src/soulfire/instance_pb2.pyi +441 -0
  39. soulfire-0.1.1/src/soulfire/login_connect.py +270 -0
  40. soulfire-0.1.1/src/soulfire/login_pb2.py +70 -0
  41. soulfire-0.1.1/src/soulfire/login_pb2.pyi +53 -0
  42. soulfire-0.1.1/src/soulfire/logs_connect.py +270 -0
  43. soulfire-0.1.1/src/soulfire/logs_pb2.py +93 -0
  44. soulfire-0.1.1/src/soulfire/logs_pb2.pyi +108 -0
  45. soulfire-0.1.1/src/soulfire/mc_auth_connect.py +335 -0
  46. soulfire-0.1.1/src/soulfire/mc_auth_pb2.py +93 -0
  47. soulfire-0.1.1/src/soulfire/mc_auth_pb2.pyi +87 -0
  48. soulfire-0.1.1/src/soulfire/metrics_connect.py +270 -0
  49. soulfire-0.1.1/src/soulfire/metrics_pb2.py +161 -0
  50. soulfire-0.1.1/src/soulfire/metrics_pb2.pyi +165 -0
  51. soulfire-0.1.1/src/soulfire/plugin_stats_connect.py +205 -0
  52. soulfire-0.1.1/src/soulfire/plugin_stats_pb2.py +67 -0
  53. soulfire-0.1.1/src/soulfire/plugin_stats_pb2.pyi +53 -0
  54. soulfire-0.1.1/src/soulfire/proxy_check_connect.py +205 -0
  55. soulfire-0.1.1/src/soulfire/proxy_check_pb2.py +62 -0
  56. soulfire-0.1.1/src/soulfire/proxy_check_pb2.pyi +42 -0
  57. soulfire-0.1.1/src/soulfire/py.typed +1 -0
  58. soulfire-0.1.1/src/soulfire/script_connect.py +985 -0
  59. soulfire-0.1.1/src/soulfire/script_pb2.py +446 -0
  60. soulfire-0.1.1/src/soulfire/script_pb2.pyi +668 -0
  61. soulfire-0.1.1/src/soulfire/server_connect.py +335 -0
  62. soulfire-0.1.1/src/soulfire/server_pb2.py +80 -0
  63. soulfire-0.1.1/src/soulfire/server_pb2.pyi +58 -0
  64. soulfire-0.1.1/src/soulfire/user_connect.py +595 -0
  65. soulfire-0.1.1/src/soulfire/user_pb2.py +140 -0
  66. soulfire-0.1.1/src/soulfire/user_pb2.pyi +131 -0
  67. soulfire-0.1.1/tests/test_auth.py +32 -0
  68. soulfire-0.1.1/tests/test_bot.py +134 -0
  69. soulfire-0.1.1/tests/test_client.py +179 -0
  70. soulfire-0.1.1/tests/test_install.py +32 -0
@@ -0,0 +1,61 @@
1
+ # Eclipse stuff
2
+ /.classpath
3
+ /.project
4
+ /.settings
5
+
6
+ # NetBeans
7
+ */nbproject
8
+ nb-configuration.xml
9
+
10
+ # maven
11
+ */target
12
+
13
+ # vim
14
+ .*.sw[a-p]
15
+
16
+ # virtual machine crash logs, see https://www.java.com/en/download/help/error_hotspot.xml
17
+ hs_err_pid*
18
+
19
+ # various other potential build files
20
+ */build/
21
+ /bin
22
+ /dist
23
+ /manifest.mf
24
+ *.log
25
+
26
+ # Mac filesystem dust
27
+ .DS_Store
28
+
29
+ # IntelliJ
30
+ *.iml
31
+ *.ipr
32
+ *.iws
33
+ .idea/
34
+
35
+ # Gradle
36
+ .kotlin
37
+ .gradle
38
+
39
+ # Ignore Gradle GUI config
40
+ gradle-app.setting
41
+
42
+ # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
43
+ !gradle-wrapper.jar
44
+
45
+ **/build/
46
+ **/run/
47
+ out/
48
+
49
+ temp/
50
+ tmp/
51
+ .fabric
52
+
53
+ # SDK toolchains
54
+ .soulfire/
55
+ node_modules/
56
+ sdk/typescript/dist/
57
+ sdk/python/.venv/
58
+ sdk/python/.pytest_cache/
59
+ sdk/python/.ruff_cache/
60
+ sdk/python/dist/
61
+ **/__pycache__/
@@ -0,0 +1,256 @@
1
+ Metadata-Version: 2.4
2
+ Name: soulfire
3
+ Version: 0.1.1
4
+ Summary: Python SDK for automating SoulFire bots over gRPC-Web.
5
+ Project-URL: Homepage, https://soulfiremc.com
6
+ Project-URL: Repository, https://github.com/soulfiremc-com/SoulFire
7
+ Author: SoulFire
8
+ License-Expression: AGPL-3.0-only
9
+ Requires-Python: >=3.10
10
+ Requires-Dist: connectrpc<0.12,>=0.11.1
11
+ Requires-Dist: googleapis-common-protos<2,>=1.70
12
+ Requires-Dist: protobuf<8,>=6.31.1
13
+ Provides-Extra: dev
14
+ Requires-Dist: hatchling>=1.27; extra == 'dev'
15
+ Requires-Dist: pytest-asyncio<2,>=1.2; extra == 'dev'
16
+ Requires-Dist: pytest<10,>=9.1; extra == 'dev'
17
+ Requires-Dist: ruff<0.17,>=0.16; extra == 'dev'
18
+ Description-Content-Type: text/markdown
19
+
20
+ # SoulFire Python SDK
21
+
22
+ Use `soulfire` to control bots through a SoulFire server from synchronous
23
+ or asyncio Python applications. The SDK always uses gRPC-Web.
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ pip install soulfire
29
+ ```
30
+
31
+ ## Connect and stream events
32
+
33
+ Create a client with an API token, then select an operator-provisioned instance
34
+ and bot:
35
+
36
+ ```python
37
+ import asyncio
38
+ import os
39
+
40
+ from soulfire import SoulFire
41
+
42
+
43
+ async def main() -> None:
44
+ async with SoulFire.connect(
45
+ "https://soulfire.example.com",
46
+ token=os.environ["SOULFIRE_TOKEN"],
47
+ ) as soulfire:
48
+ bot = soulfire.instance("instance-uuid").bot("bot-uuid")
49
+ await bot.start()
50
+
51
+ async for event in bot.events():
52
+ print(event)
53
+
54
+
55
+ asyncio.run(main())
56
+ ```
57
+
58
+ The default event filter includes status, state, inventory, damage, chat, and
59
+ lifecycle events. The stream stays open while the bot is stopped and follows
60
+ the configured account across reconnects.
61
+
62
+ ## Control bots
63
+
64
+ Bot intent is persistent. Calling `start()` sets the bot's desired state to
65
+ running, while `stop()` sets it to stopped:
66
+
67
+ ```python
68
+ instance = soulfire.instance("instance-uuid")
69
+ bot = instance.bot("bot-uuid")
70
+
71
+ await bot.start()
72
+ await bot.restart()
73
+ await bot.stop()
74
+ ```
75
+
76
+ Control a group by explicit ID or count:
77
+
78
+ ```python
79
+ await instance.start(count=25)
80
+ await instance.stop(bot_ids=["bot-uuid-1", "bot-uuid-2"])
81
+ await instance.restart()
82
+ ```
83
+
84
+ With no selection, `start()` targets stopped bots, `stop()` targets desired
85
+ bots, and `restart()` targets desired bots. A `count` selection follows the
86
+ instance's `account.shuffle-accounts` setting. Explicit `bot_ids` always use
87
+ the IDs you provide.
88
+
89
+ ## Watch bot status
90
+
91
+ `watch_bot_statuses()` first yields a complete snapshot, then incremental
92
+ updates and removals:
93
+
94
+ ```python
95
+ async for event in instance.watch_bot_statuses():
96
+ event_type = event.WhichOneof("event")
97
+ if event_type == "snapshot":
98
+ print(event.snapshot.bots)
99
+ elif event_type == "update":
100
+ print(event.update.profile_id, event.update.runtime_state)
101
+ elif event_type == "removed_bot_id":
102
+ print("Removed", event.removed_bot_id)
103
+ ```
104
+
105
+ Use `await bot.status()` when you only need the current state of one bot. The
106
+ synchronous client exposes the same methods without `await`.
107
+
108
+ ## Provision a local server
109
+
110
+ `SoulFire.install()` downloads and verifies the latest SoulFire dedicated
111
+ server and a Temurin 25 runtime, starts the server, and returns an authenticated
112
+ client:
113
+
114
+ ```python
115
+ import asyncio
116
+
117
+ from soulfire import SoulFire
118
+
119
+
120
+ async def main() -> None:
121
+ soulfire = await SoulFire.install(
122
+ directory=".soulfire",
123
+ on_log=print,
124
+ )
125
+ try:
126
+ print(soulfire.local_server.base_url)
127
+ bot = soulfire.instance("instance-uuid").bot("bot-uuid")
128
+ # Use the operator-provisioned bot.
129
+ finally:
130
+ await soulfire.close()
131
+
132
+
133
+ asyncio.run(main())
134
+ ```
135
+
136
+ Pass `version` to pin a release tag. Existing verified downloads are reused.
137
+
138
+ ## Send a chat message
139
+
140
+ ```python
141
+ await bot.send_chat("Hello from the SoulFire SDK")
142
+ ```
143
+
144
+ Action calls return after the bot game thread executes them. Failed or
145
+ cancelled actions raise `SoulFireActionError`, which includes the action ID and
146
+ server result.
147
+
148
+ ## Control inventory and movement
149
+
150
+ ```python
151
+ from soulfire.bot_pb2 import LEFT_CLICK
152
+
153
+ inventory = await bot.inventory()
154
+ print(inventory.slots)
155
+
156
+ await bot.select_hotbar(0)
157
+ await bot.click_inventory(12, LEFT_CLICK)
158
+ await bot.transfer_inventory_slot(12)
159
+ await bot.move_inventory_stack(12, 36)
160
+
161
+ await bot.set_movement(forward=True, sprint=True)
162
+ await bot.look(90, 0)
163
+ await bot.reset_movement()
164
+ ```
165
+
166
+ The synchronous bot exposes the same methods without `await`.
167
+
168
+ ## Coordinate multiple controllers
169
+
170
+ ```python
171
+ lease = await bot.acquire_control(ttl_seconds=30)
172
+ try:
173
+ await bot.send_chat("This action carries the lease token")
174
+ await lease.renew(ttl_seconds=30)
175
+ finally:
176
+ await lease.release()
177
+ ```
178
+
179
+ Control leases are optional. Acquiring one prevents other clients from issuing
180
+ actions until the lease is released or expires.
181
+
182
+ ## Use composable behaviors
183
+
184
+ ```python
185
+ from soulfire import AttackNearest, CollectBlocks, run_behaviors
186
+
187
+ await run_behaviors(
188
+ bot,
189
+ [
190
+ CollectBlocks(["minecraft:oak_log"], max_count=16),
191
+ AttackNearest(["minecraft:zombie"]),
192
+ ],
193
+ )
194
+ ```
195
+
196
+ The package includes `CollectBlocks`, `FollowEntity`, `AttackNearest`,
197
+ `AutoEat`, and `Build`.
198
+
199
+ ## Provision instances and accounts
200
+
201
+ ```python
202
+ from soulfire.common_pb2 import MICROSOFT_JAVA_DEVICE_CODE
203
+
204
+ instance = await soulfire.create_instance("automation")
205
+
206
+ async for step in instance.login_device_code(MICROSOFT_JAVA_DEVICE_CODE):
207
+ if step.WhichOneof("data") == "device_code":
208
+ print(step.device_code.verification_uri, step.device_code.user_code)
209
+ elif step.WhichOneof("data") == "account":
210
+ await instance.add_accounts([step.account])
211
+ ```
212
+
213
+ Instance listing, settings, account and proxy batches, credentials login,
214
+ device-code login, and account refresh are available on async and synchronous
215
+ clients.
216
+
217
+ ## Manage an installed server
218
+
219
+ ```python
220
+ print(soulfire.local_server.version)
221
+ print(soulfire.local_server_logs)
222
+
223
+ await soulfire.restart_local_server()
224
+ await soulfire.stop_local_server()
225
+ ```
226
+
227
+ Restart keeps the same directory, port, release, and root API token.
228
+
229
+ ## Use the synchronous client
230
+
231
+ ```python
232
+ import os
233
+
234
+ from soulfire import SoulFireSync
235
+
236
+
237
+ with SoulFireSync.connect(
238
+ "https://soulfire.example.com",
239
+ token=os.environ["SOULFIRE_TOKEN"],
240
+ ) as soulfire:
241
+ bot = soulfire.instance("instance-uuid").bot("bot-uuid")
242
+ bot.send_chat("Hello from Python")
243
+ ```
244
+
245
+ Generated request messages and service clients are importable from the
246
+ `soulfire` package when an RPC does not have a high-level wrapper:
247
+
248
+ ```python
249
+ from soulfire.instance_connect import InstanceServiceClient
250
+
251
+ instances = soulfire.service(InstanceServiceClient)
252
+ ```
253
+
254
+ Instances are not lifecycle units. They compartmentalize accounts, settings,
255
+ proxies, permissions, scripts, and automation. Each bot can be controlled
256
+ independently.
@@ -0,0 +1,237 @@
1
+ # SoulFire Python SDK
2
+
3
+ Use `soulfire` to control bots through a SoulFire server from synchronous
4
+ or asyncio Python applications. The SDK always uses gRPC-Web.
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ pip install soulfire
10
+ ```
11
+
12
+ ## Connect and stream events
13
+
14
+ Create a client with an API token, then select an operator-provisioned instance
15
+ and bot:
16
+
17
+ ```python
18
+ import asyncio
19
+ import os
20
+
21
+ from soulfire import SoulFire
22
+
23
+
24
+ async def main() -> None:
25
+ async with SoulFire.connect(
26
+ "https://soulfire.example.com",
27
+ token=os.environ["SOULFIRE_TOKEN"],
28
+ ) as soulfire:
29
+ bot = soulfire.instance("instance-uuid").bot("bot-uuid")
30
+ await bot.start()
31
+
32
+ async for event in bot.events():
33
+ print(event)
34
+
35
+
36
+ asyncio.run(main())
37
+ ```
38
+
39
+ The default event filter includes status, state, inventory, damage, chat, and
40
+ lifecycle events. The stream stays open while the bot is stopped and follows
41
+ the configured account across reconnects.
42
+
43
+ ## Control bots
44
+
45
+ Bot intent is persistent. Calling `start()` sets the bot's desired state to
46
+ running, while `stop()` sets it to stopped:
47
+
48
+ ```python
49
+ instance = soulfire.instance("instance-uuid")
50
+ bot = instance.bot("bot-uuid")
51
+
52
+ await bot.start()
53
+ await bot.restart()
54
+ await bot.stop()
55
+ ```
56
+
57
+ Control a group by explicit ID or count:
58
+
59
+ ```python
60
+ await instance.start(count=25)
61
+ await instance.stop(bot_ids=["bot-uuid-1", "bot-uuid-2"])
62
+ await instance.restart()
63
+ ```
64
+
65
+ With no selection, `start()` targets stopped bots, `stop()` targets desired
66
+ bots, and `restart()` targets desired bots. A `count` selection follows the
67
+ instance's `account.shuffle-accounts` setting. Explicit `bot_ids` always use
68
+ the IDs you provide.
69
+
70
+ ## Watch bot status
71
+
72
+ `watch_bot_statuses()` first yields a complete snapshot, then incremental
73
+ updates and removals:
74
+
75
+ ```python
76
+ async for event in instance.watch_bot_statuses():
77
+ event_type = event.WhichOneof("event")
78
+ if event_type == "snapshot":
79
+ print(event.snapshot.bots)
80
+ elif event_type == "update":
81
+ print(event.update.profile_id, event.update.runtime_state)
82
+ elif event_type == "removed_bot_id":
83
+ print("Removed", event.removed_bot_id)
84
+ ```
85
+
86
+ Use `await bot.status()` when you only need the current state of one bot. The
87
+ synchronous client exposes the same methods without `await`.
88
+
89
+ ## Provision a local server
90
+
91
+ `SoulFire.install()` downloads and verifies the latest SoulFire dedicated
92
+ server and a Temurin 25 runtime, starts the server, and returns an authenticated
93
+ client:
94
+
95
+ ```python
96
+ import asyncio
97
+
98
+ from soulfire import SoulFire
99
+
100
+
101
+ async def main() -> None:
102
+ soulfire = await SoulFire.install(
103
+ directory=".soulfire",
104
+ on_log=print,
105
+ )
106
+ try:
107
+ print(soulfire.local_server.base_url)
108
+ bot = soulfire.instance("instance-uuid").bot("bot-uuid")
109
+ # Use the operator-provisioned bot.
110
+ finally:
111
+ await soulfire.close()
112
+
113
+
114
+ asyncio.run(main())
115
+ ```
116
+
117
+ Pass `version` to pin a release tag. Existing verified downloads are reused.
118
+
119
+ ## Send a chat message
120
+
121
+ ```python
122
+ await bot.send_chat("Hello from the SoulFire SDK")
123
+ ```
124
+
125
+ Action calls return after the bot game thread executes them. Failed or
126
+ cancelled actions raise `SoulFireActionError`, which includes the action ID and
127
+ server result.
128
+
129
+ ## Control inventory and movement
130
+
131
+ ```python
132
+ from soulfire.bot_pb2 import LEFT_CLICK
133
+
134
+ inventory = await bot.inventory()
135
+ print(inventory.slots)
136
+
137
+ await bot.select_hotbar(0)
138
+ await bot.click_inventory(12, LEFT_CLICK)
139
+ await bot.transfer_inventory_slot(12)
140
+ await bot.move_inventory_stack(12, 36)
141
+
142
+ await bot.set_movement(forward=True, sprint=True)
143
+ await bot.look(90, 0)
144
+ await bot.reset_movement()
145
+ ```
146
+
147
+ The synchronous bot exposes the same methods without `await`.
148
+
149
+ ## Coordinate multiple controllers
150
+
151
+ ```python
152
+ lease = await bot.acquire_control(ttl_seconds=30)
153
+ try:
154
+ await bot.send_chat("This action carries the lease token")
155
+ await lease.renew(ttl_seconds=30)
156
+ finally:
157
+ await lease.release()
158
+ ```
159
+
160
+ Control leases are optional. Acquiring one prevents other clients from issuing
161
+ actions until the lease is released or expires.
162
+
163
+ ## Use composable behaviors
164
+
165
+ ```python
166
+ from soulfire import AttackNearest, CollectBlocks, run_behaviors
167
+
168
+ await run_behaviors(
169
+ bot,
170
+ [
171
+ CollectBlocks(["minecraft:oak_log"], max_count=16),
172
+ AttackNearest(["minecraft:zombie"]),
173
+ ],
174
+ )
175
+ ```
176
+
177
+ The package includes `CollectBlocks`, `FollowEntity`, `AttackNearest`,
178
+ `AutoEat`, and `Build`.
179
+
180
+ ## Provision instances and accounts
181
+
182
+ ```python
183
+ from soulfire.common_pb2 import MICROSOFT_JAVA_DEVICE_CODE
184
+
185
+ instance = await soulfire.create_instance("automation")
186
+
187
+ async for step in instance.login_device_code(MICROSOFT_JAVA_DEVICE_CODE):
188
+ if step.WhichOneof("data") == "device_code":
189
+ print(step.device_code.verification_uri, step.device_code.user_code)
190
+ elif step.WhichOneof("data") == "account":
191
+ await instance.add_accounts([step.account])
192
+ ```
193
+
194
+ Instance listing, settings, account and proxy batches, credentials login,
195
+ device-code login, and account refresh are available on async and synchronous
196
+ clients.
197
+
198
+ ## Manage an installed server
199
+
200
+ ```python
201
+ print(soulfire.local_server.version)
202
+ print(soulfire.local_server_logs)
203
+
204
+ await soulfire.restart_local_server()
205
+ await soulfire.stop_local_server()
206
+ ```
207
+
208
+ Restart keeps the same directory, port, release, and root API token.
209
+
210
+ ## Use the synchronous client
211
+
212
+ ```python
213
+ import os
214
+
215
+ from soulfire import SoulFireSync
216
+
217
+
218
+ with SoulFireSync.connect(
219
+ "https://soulfire.example.com",
220
+ token=os.environ["SOULFIRE_TOKEN"],
221
+ ) as soulfire:
222
+ bot = soulfire.instance("instance-uuid").bot("bot-uuid")
223
+ bot.send_chat("Hello from Python")
224
+ ```
225
+
226
+ Generated request messages and service clients are importable from the
227
+ `soulfire` package when an RPC does not have a high-level wrapper:
228
+
229
+ ```python
230
+ from soulfire.instance_connect import InstanceServiceClient
231
+
232
+ instances = soulfire.service(InstanceServiceClient)
233
+ ```
234
+
235
+ Instances are not lifecycle units. They compartmentalize accounts, settings,
236
+ proxies, permissions, scripts, and automation. Each bot can be controlled
237
+ independently.
@@ -0,0 +1,51 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "soulfire"
7
+ version = "0.1.1"
8
+ description = "Python SDK for automating SoulFire bots over gRPC-Web."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "AGPL-3.0-only"
12
+ authors = [
13
+ { name = "SoulFire" },
14
+ ]
15
+ dependencies = [
16
+ "connectrpc>=0.11.1,<0.12",
17
+ "googleapis-common-protos>=1.70,<2",
18
+ "protobuf>=6.31.1,<8",
19
+ ]
20
+
21
+ [project.urls]
22
+ Homepage = "https://soulfiremc.com"
23
+ Repository = "https://github.com/soulfiremc-com/SoulFire"
24
+
25
+ [project.optional-dependencies]
26
+ dev = [
27
+ "hatchling>=1.27",
28
+ "pytest>=9.1,<10",
29
+ "pytest-asyncio>=1.2,<2",
30
+ "ruff>=0.16,<0.17",
31
+ ]
32
+
33
+ [tool.hatch.build.targets.wheel]
34
+ packages = ["src/soulfire"]
35
+
36
+ [tool.pytest.ini_options]
37
+ addopts = "-q"
38
+ asyncio_mode = "auto"
39
+ testpaths = ["tests"]
40
+
41
+ [tool.ruff]
42
+ extend-exclude = [
43
+ "src/soulfire/*_connect.py",
44
+ "src/soulfire/*_pb2.py",
45
+ "src/soulfire/*_pb2.pyi",
46
+ ]
47
+ line-length = 100
48
+ target-version = "py310"
49
+
50
+ [tool.ruff.lint]
51
+ select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
@@ -0,0 +1,38 @@
1
+ from ._install import LocalSoulFireServer
2
+ from .behaviors import (
3
+ AttackNearest,
4
+ AutoEat,
5
+ BotBehavior,
6
+ Build,
7
+ BuildPlacement,
8
+ CollectBlocks,
9
+ FollowEntity,
10
+ run_behaviors,
11
+ )
12
+ from .bot import (
13
+ SoulFireActionError,
14
+ SoulFireBot,
15
+ SoulFireBotControlLease,
16
+ SoulFireBotControlLeaseSync,
17
+ SoulFireBotSync,
18
+ )
19
+ from .client import SoulFire, SoulFireSync
20
+
21
+ __all__ = [
22
+ "AttackNearest",
23
+ "AutoEat",
24
+ "BotBehavior",
25
+ "Build",
26
+ "BuildPlacement",
27
+ "CollectBlocks",
28
+ "FollowEntity",
29
+ "LocalSoulFireServer",
30
+ "SoulFire",
31
+ "SoulFireActionError",
32
+ "SoulFireBot",
33
+ "SoulFireBotControlLease",
34
+ "SoulFireBotControlLeaseSync",
35
+ "SoulFireBotSync",
36
+ "SoulFireSync",
37
+ "run_behaviors",
38
+ ]
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from typing import TypeAlias
5
+
6
+ from connectrpc.request import RequestContext
7
+
8
+ TokenProvider: TypeAlias = str | Callable[[], str | None]
9
+
10
+
11
+ def resolve_token(token: TokenProvider | None) -> str | None:
12
+ if callable(token):
13
+ return token()
14
+ return token
15
+
16
+
17
+ class BearerAuthInterceptor:
18
+ def __init__(self, token_provider: Callable[[], TokenProvider | None]) -> None:
19
+ self._token_provider = token_provider
20
+
21
+ async def on_start(self, ctx: RequestContext) -> None:
22
+ token = resolve_token(self._token_provider())
23
+ if token:
24
+ ctx.request_headers["Authorization"] = f"Bearer {token}"
25
+
26
+ async def on_end(
27
+ self,
28
+ _token: None,
29
+ _ctx: RequestContext,
30
+ _error: Exception | None,
31
+ ) -> None:
32
+ return None
33
+
34
+
35
+ class BearerAuthInterceptorSync:
36
+ def __init__(self, token_provider: Callable[[], TokenProvider | None]) -> None:
37
+ self._token_provider = token_provider
38
+
39
+ def on_start_sync(self, ctx: RequestContext) -> None:
40
+ token = resolve_token(self._token_provider())
41
+ if token:
42
+ ctx.request_headers["Authorization"] = f"Bearer {token}"
43
+
44
+ def on_end_sync(
45
+ self,
46
+ _token: None,
47
+ _ctx: RequestContext,
48
+ _error: Exception | None,
49
+ ) -> None:
50
+ return None