dst-server 0.2.1__tar.gz → 0.2.2__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.
- {dst_server-0.2.1 → dst_server-0.2.2}/PKG-INFO +34 -15
- {dst_server-0.2.1 → dst_server-0.2.2}/README.md +33 -14
- {dst_server-0.2.1 → dst_server-0.2.2}/pyproject.toml +1 -1
- {dst_server-0.2.1 → dst_server-0.2.2}/pyproject.toml.orig +1 -1
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/archive.py +26 -4
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/cluster/daemon.py +2 -5
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/commands.py +0 -15
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/errors.py +0 -1
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/klei/lobby.py +9 -6
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/models/snapshot.py +8 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/rpc/__init__.py +1 -2
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/rpc/client.py +2 -5
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/rpc/schema/rpc.capnp +2 -2
- dst_server-0.2.2/src/dst_server/rpc/schema.py +12 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/rpc/servants.py +4 -26
- dst_server-0.2.1/src/dst_server/rpc/schema.py +0 -43
- {dst_server-0.2.1 → dst_server-0.2.2}/LICENSE +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/__init__.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/annotations/__init__.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/annotations/cli.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/annotations/generator.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/annotations/values.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/annotations/visitors.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/api.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/cluster/__init__.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/cluster/agent.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/cluster/cli.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/cluster/console.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/cluster/controller.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/cluster/service.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/cluster/subscriptions.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/concurrency.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/configuration/__init__.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/configuration/files.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/configuration/ini.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/configuration/models.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/configuration/overrides.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/configuration/presets.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/configuration/store.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/configuration/world.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/deployment/__init__.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/deployment/application.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/deployment/models.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/deployment/quadlet.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/events/__init__.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/events/base.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/events/player.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/events/record.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/events/server.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/events/world.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/game/__init__.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/game/client.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/game/enums.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/game/rpc.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/klei/__init__.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/klei/client.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/klei/enums.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/klei/schema.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/klei/version.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/klei_id.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/lua/dst_server/actions.lua +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/lua/dst_server/commands.lua +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/lua/dst_server/player_events.lua +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/lua/dst_server/player_queries.lua +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/lua/dst_server/player_values.lua +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/lua/dst_server/state.lua +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/lua/dst_server/telemetry.lua +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/lua/dst_server/values.lua +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/lua/dst_server/wire.lua +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/lua/dst_server/world_events.lua +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/lua/dst_server/world_queries.lua +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/lua/dst_server.lua +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/lua_codec.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/models/__init__.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/models/base.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/models/cluster.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/models/driver.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/models/entity.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/models/player.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/models/server.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/models/telemetry.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/mods/__init__.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/mods/files.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/mods/native.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/mods/process.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/mods/steamcmd.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/mods/workshop.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/netdata.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/py.typed +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/rpc/codec.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/rpc/transport.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/runtime/__init__.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/runtime/config.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/runtime/console.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/runtime/driver.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/runtime/fds.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/runtime/lifecycle.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/runtime/operational.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/runtime/request.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/runtime/server.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/runtime/supervisor.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/telemetry/__init__.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/telemetry/config.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/telemetry/exporter.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/telemetry/otel.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/telemetry/outbox.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/telemetry/recorder.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/telemetry/stream.py +0 -0
- {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/timeouts.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dst-server
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Start and control a Don't Starve Together server process.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -383,7 +383,7 @@ The master Agent registers in-process; secondary Agents register through the Pod
|
|
|
383
383
|
|
|
384
384
|
| Channel | Purpose |
|
|
385
385
|
| --- | --- |
|
|
386
|
-
| `/cluster/.dst-server.sock` | Public Cap'n Proto RPC
|
|
386
|
+
| `/cluster/.dst-server.sock` | Public Cap'n Proto RPC. |
|
|
387
387
|
| Game FD 3 | Lua command input. |
|
|
388
388
|
| Game FD 4 | Command text output; raw Lua must explicitly `print`. |
|
|
389
389
|
| Game FD 5 | Native lifecycle events such as Ready, Session, Saved, and Stopping. |
|
|
@@ -582,8 +582,6 @@ Each endpoint accepts only its declared command scope.
|
|
|
582
582
|
Game clients handle game operations; controllers own process lifecycle and cluster coordination.
|
|
583
583
|
|
|
584
584
|
Cap'n Proto carries commands through `call` and observations through subscription capabilities.
|
|
585
|
-
The handshake fingerprint covers the capability schema and Pydantic request, result, event, and error schemas.
|
|
586
|
-
Use matching client and daemon versions.
|
|
587
585
|
Configuration payloads preserve omitted fields, explicit `False`, world override types, and secrets needed for saving.
|
|
588
586
|
|
|
589
587
|
Import cluster results, statuses, and observation cursors from [models.cluster](src/dst_server/models/cluster.py).
|
|
@@ -773,7 +771,9 @@ if catalog.has_more and catalog.snapshots:
|
|
|
773
771
|
The Agent rejects path escapes, symlinks, invalid metadata, and session changes during a query.
|
|
774
772
|
For standalone reads, use `WorldSnapshotMetadata.load(path)` / `PlayerSnapshotMetadata.load(path)` from [models.snapshot](src/dst_server/models/snapshot.py).
|
|
775
773
|
The loaders parse only UTF-8 Lua literals and support native text headers and trailing NULs.
|
|
776
|
-
|
|
774
|
+
Additional fields in `clock` and `seasons` written by Mods are ignored; known fields retain strict validation.
|
|
775
|
+
Unknown fields elsewhere, wrong types, and dynamic expressions are rejected.
|
|
776
|
+
The day uses the standard `clock.cycles + 1`, without interpreting Mod calendars.
|
|
777
777
|
The world model covers `clock`, `seasons`, and nested fields.
|
|
778
778
|
The player model exposes `character`, including Mod character identifiers.
|
|
779
779
|
|
|
@@ -838,22 +838,21 @@ Export detects file changes but cannot guarantee an atomic snapshot of an online
|
|
|
838
838
|
The input must remain unchanged.
|
|
839
839
|
Exports and uploads are available; there is no import API yet.
|
|
840
840
|
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
```shell
|
|
844
|
-
export AWS_ENDPOINT='https://<account-id>.r2.cloudflarestorage.com'
|
|
845
|
-
export AWS_BUCKET='your-bucket'
|
|
846
|
-
export AWS_ACCESS_KEY_ID='your-access-key-id'
|
|
847
|
-
export AWS_SECRET_ACCESS_KEY='your-secret-access-key'
|
|
848
|
-
```
|
|
841
|
+
Pass S3 connection settings and credentials directly when uploading to R2:
|
|
849
842
|
|
|
850
843
|
```python
|
|
851
844
|
from pathlib import Path
|
|
852
845
|
|
|
846
|
+
from pydantic import SecretStr
|
|
847
|
+
|
|
853
848
|
from dst_server.archive import export_cluster
|
|
854
849
|
|
|
855
850
|
with export_cluster(Path("/srv/dst/000")) as archive:
|
|
856
851
|
result = archive.upload(
|
|
852
|
+
endpoint="https://<account-id>.r2.cloudflarestorage.com",
|
|
853
|
+
bucket="your-bucket",
|
|
854
|
+
access_key_id=SecretStr("your-access-key-id"),
|
|
855
|
+
secret_access_key=SecretStr("your-secret-access-key"),
|
|
857
856
|
object_prefix="rooms/exports/",
|
|
858
857
|
url_prefix="https://downloads.example.com/",
|
|
859
858
|
)
|
|
@@ -862,6 +861,25 @@ print(result.key)
|
|
|
862
861
|
print(result.url)
|
|
863
862
|
```
|
|
864
863
|
|
|
864
|
+
| Upload argument | Type | Default | Environment fallback |
|
|
865
|
+
| --- | --- | --- | --- |
|
|
866
|
+
| `bucket` | `str \| None` | `None` | `AWS_BUCKET` |
|
|
867
|
+
| `endpoint` | `str \| None` | `None` | `AWS_ENDPOINT_URL_S3`, then `AWS_ENDPOINT` |
|
|
868
|
+
| `region` | `str` | `"auto"` | None; the argument overrides `AWS_REGION` |
|
|
869
|
+
| `access_key_id` | `SecretStr \| None` | `None` | `AWS_ACCESS_KEY_ID` |
|
|
870
|
+
| `secret_access_key` | `SecretStr \| None` | `None` | `AWS_SECRET_ACCESS_KEY` |
|
|
871
|
+
| `session_token` | `SecretStr \| None` | `None` | `AWS_SESSION_TOKEN` |
|
|
872
|
+
| `object_prefix` | `str` | `""` | None |
|
|
873
|
+
| `url_prefix` | `str \| None` | `None` | None |
|
|
874
|
+
|
|
875
|
+
The three credential arguments require `SecretStr` instances; plain strings are rejected.
|
|
876
|
+
Secrets are unwrapped only when creating `S3Store` and are never written to the archive.
|
|
877
|
+
Explicit values override the corresponding environment settings, including `AWS_ENDPOINT_URL_S3` for `endpoint`.
|
|
878
|
+
`None` leaves that field to [obstore's environment configuration](https://developmentseed.org/obstore/latest/api/store/aws/#obstore.store.S3Config).
|
|
879
|
+
This fallback applies per field: an omitted `session_token` can still come from the environment when both keys are explicit.
|
|
880
|
+
Other obstore options retain their environment behavior.
|
|
881
|
+
Calling `upload()` without connection or credential arguments continues to use AWS environment variables.
|
|
882
|
+
|
|
865
883
|
`upload()` reads from the start of the stream and returns `ArchiveUploadResult` with `key` and `url` fields.
|
|
866
884
|
The object key is `object_prefix + archive.filename`; `object_prefix` defaults to an empty string.
|
|
867
885
|
The filename remains `DST-<room-id>-<UTC timestamp>.7z`, with the timestamp precise to seconds.
|
|
@@ -871,8 +889,9 @@ Both prefixes are explicit SDK arguments and are concatenated literally.
|
|
|
871
889
|
`object_prefix` must not begin with `/`, which the storage backend would otherwise strip from the key.
|
|
872
890
|
Supply any required separators, such as `/` or `?file=`, yourself.
|
|
873
891
|
Query prefixes and trailing separators are preserved, and the URL is never inferred from the bucket or S3 endpoint.
|
|
874
|
-
|
|
875
|
-
`
|
|
892
|
+
The two prefixes are ordinary strings and have no corresponding environment variables.
|
|
893
|
+
The default `region="auto"` uses [R2's `auto` region](https://developers.cloudflare.com/r2/api/s3/api/#bucket-region).
|
|
894
|
+
Pass `region` explicitly when uploading to a different S3 region.
|
|
876
895
|
[obstore handles multipart uploads](https://developmentseed.org/obstore/latest/api/put/); errors propagate to the caller, and local temporary files are still cleaned up.
|
|
877
896
|
Remote parts from failed uploads may remain; R2 removes them after seven days by default.
|
|
878
897
|
Configure this through [lifecycle rules](https://developers.cloudflare.com/r2/buckets/object-lifecycles/).
|
|
@@ -360,7 +360,7 @@ The master Agent registers in-process; secondary Agents register through the Pod
|
|
|
360
360
|
|
|
361
361
|
| Channel | Purpose |
|
|
362
362
|
| --- | --- |
|
|
363
|
-
| `/cluster/.dst-server.sock` | Public Cap'n Proto RPC
|
|
363
|
+
| `/cluster/.dst-server.sock` | Public Cap'n Proto RPC. |
|
|
364
364
|
| Game FD 3 | Lua command input. |
|
|
365
365
|
| Game FD 4 | Command text output; raw Lua must explicitly `print`. |
|
|
366
366
|
| Game FD 5 | Native lifecycle events such as Ready, Session, Saved, and Stopping. |
|
|
@@ -559,8 +559,6 @@ Each endpoint accepts only its declared command scope.
|
|
|
559
559
|
Game clients handle game operations; controllers own process lifecycle and cluster coordination.
|
|
560
560
|
|
|
561
561
|
Cap'n Proto carries commands through `call` and observations through subscription capabilities.
|
|
562
|
-
The handshake fingerprint covers the capability schema and Pydantic request, result, event, and error schemas.
|
|
563
|
-
Use matching client and daemon versions.
|
|
564
562
|
Configuration payloads preserve omitted fields, explicit `False`, world override types, and secrets needed for saving.
|
|
565
563
|
|
|
566
564
|
Import cluster results, statuses, and observation cursors from [models.cluster](src/dst_server/models/cluster.py).
|
|
@@ -750,7 +748,9 @@ if catalog.has_more and catalog.snapshots:
|
|
|
750
748
|
The Agent rejects path escapes, symlinks, invalid metadata, and session changes during a query.
|
|
751
749
|
For standalone reads, use `WorldSnapshotMetadata.load(path)` / `PlayerSnapshotMetadata.load(path)` from [models.snapshot](src/dst_server/models/snapshot.py).
|
|
752
750
|
The loaders parse only UTF-8 Lua literals and support native text headers and trailing NULs.
|
|
753
|
-
|
|
751
|
+
Additional fields in `clock` and `seasons` written by Mods are ignored; known fields retain strict validation.
|
|
752
|
+
Unknown fields elsewhere, wrong types, and dynamic expressions are rejected.
|
|
753
|
+
The day uses the standard `clock.cycles + 1`, without interpreting Mod calendars.
|
|
754
754
|
The world model covers `clock`, `seasons`, and nested fields.
|
|
755
755
|
The player model exposes `character`, including Mod character identifiers.
|
|
756
756
|
|
|
@@ -815,22 +815,21 @@ Export detects file changes but cannot guarantee an atomic snapshot of an online
|
|
|
815
815
|
The input must remain unchanged.
|
|
816
816
|
Exports and uploads are available; there is no import API yet.
|
|
817
817
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
```shell
|
|
821
|
-
export AWS_ENDPOINT='https://<account-id>.r2.cloudflarestorage.com'
|
|
822
|
-
export AWS_BUCKET='your-bucket'
|
|
823
|
-
export AWS_ACCESS_KEY_ID='your-access-key-id'
|
|
824
|
-
export AWS_SECRET_ACCESS_KEY='your-secret-access-key'
|
|
825
|
-
```
|
|
818
|
+
Pass S3 connection settings and credentials directly when uploading to R2:
|
|
826
819
|
|
|
827
820
|
```python
|
|
828
821
|
from pathlib import Path
|
|
829
822
|
|
|
823
|
+
from pydantic import SecretStr
|
|
824
|
+
|
|
830
825
|
from dst_server.archive import export_cluster
|
|
831
826
|
|
|
832
827
|
with export_cluster(Path("/srv/dst/000")) as archive:
|
|
833
828
|
result = archive.upload(
|
|
829
|
+
endpoint="https://<account-id>.r2.cloudflarestorage.com",
|
|
830
|
+
bucket="your-bucket",
|
|
831
|
+
access_key_id=SecretStr("your-access-key-id"),
|
|
832
|
+
secret_access_key=SecretStr("your-secret-access-key"),
|
|
834
833
|
object_prefix="rooms/exports/",
|
|
835
834
|
url_prefix="https://downloads.example.com/",
|
|
836
835
|
)
|
|
@@ -839,6 +838,25 @@ print(result.key)
|
|
|
839
838
|
print(result.url)
|
|
840
839
|
```
|
|
841
840
|
|
|
841
|
+
| Upload argument | Type | Default | Environment fallback |
|
|
842
|
+
| --- | --- | --- | --- |
|
|
843
|
+
| `bucket` | `str \| None` | `None` | `AWS_BUCKET` |
|
|
844
|
+
| `endpoint` | `str \| None` | `None` | `AWS_ENDPOINT_URL_S3`, then `AWS_ENDPOINT` |
|
|
845
|
+
| `region` | `str` | `"auto"` | None; the argument overrides `AWS_REGION` |
|
|
846
|
+
| `access_key_id` | `SecretStr \| None` | `None` | `AWS_ACCESS_KEY_ID` |
|
|
847
|
+
| `secret_access_key` | `SecretStr \| None` | `None` | `AWS_SECRET_ACCESS_KEY` |
|
|
848
|
+
| `session_token` | `SecretStr \| None` | `None` | `AWS_SESSION_TOKEN` |
|
|
849
|
+
| `object_prefix` | `str` | `""` | None |
|
|
850
|
+
| `url_prefix` | `str \| None` | `None` | None |
|
|
851
|
+
|
|
852
|
+
The three credential arguments require `SecretStr` instances; plain strings are rejected.
|
|
853
|
+
Secrets are unwrapped only when creating `S3Store` and are never written to the archive.
|
|
854
|
+
Explicit values override the corresponding environment settings, including `AWS_ENDPOINT_URL_S3` for `endpoint`.
|
|
855
|
+
`None` leaves that field to [obstore's environment configuration](https://developmentseed.org/obstore/latest/api/store/aws/#obstore.store.S3Config).
|
|
856
|
+
This fallback applies per field: an omitted `session_token` can still come from the environment when both keys are explicit.
|
|
857
|
+
Other obstore options retain their environment behavior.
|
|
858
|
+
Calling `upload()` without connection or credential arguments continues to use AWS environment variables.
|
|
859
|
+
|
|
842
860
|
`upload()` reads from the start of the stream and returns `ArchiveUploadResult` with `key` and `url` fields.
|
|
843
861
|
The object key is `object_prefix + archive.filename`; `object_prefix` defaults to an empty string.
|
|
844
862
|
The filename remains `DST-<room-id>-<UTC timestamp>.7z`, with the timestamp precise to seconds.
|
|
@@ -848,8 +866,9 @@ Both prefixes are explicit SDK arguments and are concatenated literally.
|
|
|
848
866
|
`object_prefix` must not begin with `/`, which the storage backend would otherwise strip from the key.
|
|
849
867
|
Supply any required separators, such as `/` or `?file=`, yourself.
|
|
850
868
|
Query prefixes and trailing separators are preserved, and the URL is never inferred from the bucket or S3 endpoint.
|
|
851
|
-
|
|
852
|
-
`
|
|
869
|
+
The two prefixes are ordinary strings and have no corresponding environment variables.
|
|
870
|
+
The default `region="auto"` uses [R2's `auto` region](https://developers.cloudflare.com/r2/api/s3/api/#bucket-region).
|
|
871
|
+
Pass `region` explicitly when uploading to a different S3 region.
|
|
853
872
|
[obstore handles multipart uploads](https://developmentseed.org/obstore/latest/api/put/); errors propagate to the caller, and local temporary files are still cleaned up.
|
|
854
873
|
Remote parts from failed uploads may remain; R2 removes them after seven days by default.
|
|
855
874
|
Configure this through [lifecycle rules](https://developers.cloudflare.com/r2/buckets/object-lifecycles/).
|
|
@@ -10,7 +10,7 @@ from pathlib import Path
|
|
|
10
10
|
from tempfile import TemporaryFile
|
|
11
11
|
from typing import Annotated, BinaryIO
|
|
12
12
|
|
|
13
|
-
from pydantic import ConfigDict, Field, validate_call
|
|
13
|
+
from pydantic import ConfigDict, Field, InstanceOf, SecretStr, validate_call
|
|
14
14
|
|
|
15
15
|
from dst_server.klei_id import encode_klei_id
|
|
16
16
|
|
|
@@ -33,17 +33,39 @@ class ClusterArchive:
|
|
|
33
33
|
filename: str
|
|
34
34
|
stream: BinaryIO
|
|
35
35
|
|
|
36
|
-
@validate_call(config=ConfigDict(strict=True))
|
|
36
|
+
@validate_call(config=ConfigDict(strict=True, hide_input_in_errors=True))
|
|
37
37
|
def upload(
|
|
38
38
|
self,
|
|
39
39
|
*,
|
|
40
|
+
bucket: str | None = None,
|
|
41
|
+
endpoint: str | None = None,
|
|
42
|
+
region: str = "auto",
|
|
43
|
+
access_key_id: InstanceOf[SecretStr] | None = None,
|
|
44
|
+
secret_access_key: InstanceOf[SecretStr] | None = None,
|
|
45
|
+
session_token: InstanceOf[SecretStr] | None = None,
|
|
40
46
|
object_prefix: Annotated[str, Field(pattern=r"^(?:[^/]|$)")] = "",
|
|
41
47
|
url_prefix: str | None = None,
|
|
42
48
|
) -> ArchiveUploadResult:
|
|
43
|
-
"""Upload
|
|
49
|
+
"""Upload with explicit S3 settings overriding AWS environment variables."""
|
|
44
50
|
from obstore.store import S3Store
|
|
45
51
|
|
|
46
|
-
|
|
52
|
+
settings = {
|
|
53
|
+
"aws_endpoint_url_s3": endpoint,
|
|
54
|
+
"access_key_id": access_key_id,
|
|
55
|
+
"secret_access_key": secret_access_key,
|
|
56
|
+
"session_token": session_token,
|
|
57
|
+
}
|
|
58
|
+
store = S3Store(
|
|
59
|
+
bucket,
|
|
60
|
+
region=region,
|
|
61
|
+
**{
|
|
62
|
+
name: value.get_secret_value()
|
|
63
|
+
if isinstance(value, SecretStr)
|
|
64
|
+
else value
|
|
65
|
+
for name, value in settings.items()
|
|
66
|
+
if value is not None
|
|
67
|
+
},
|
|
68
|
+
)
|
|
47
69
|
key = f"{object_prefix}{self.filename}"
|
|
48
70
|
self.stream.seek(0)
|
|
49
71
|
store.put(
|
|
@@ -16,7 +16,7 @@ from dst_server.configuration.models import ShardName
|
|
|
16
16
|
from dst_server.configuration.store import ConfigurationStore
|
|
17
17
|
from dst_server.rpc.client import rpc_runtime
|
|
18
18
|
from dst_server.rpc.codec import unwrap_outcome
|
|
19
|
-
from dst_server.rpc.schema import
|
|
19
|
+
from dst_server.rpc.schema import load_schema
|
|
20
20
|
from dst_server.rpc.servants import (
|
|
21
21
|
AgentServant,
|
|
22
22
|
BootstrapServant,
|
|
@@ -355,10 +355,7 @@ async def _registered_cycle(agent: ShardAgent, internal_address: str) -> None:
|
|
|
355
355
|
stack.callback(client.close)
|
|
356
356
|
registry = client.bootstrap().cast_as(load_schema().WorkerRegistry)
|
|
357
357
|
async with asyncio.timeout(DEFAULT_LIFECYCLE_TIMEOUT):
|
|
358
|
-
response = await registry.register(
|
|
359
|
-
schemaFingerprint=SCHEMA_FINGERPRINT,
|
|
360
|
-
agent=servant,
|
|
361
|
-
)
|
|
358
|
+
response = await registry.register(agent=servant)
|
|
362
359
|
unwrap_outcome(response.result)
|
|
363
360
|
disconnected = asyncio.ensure_future(client.on_disconnect())
|
|
364
361
|
while True:
|
|
@@ -492,18 +492,3 @@ def parse_request(payload: bytes, *, scope: Scope) -> Request[Any]:
|
|
|
492
492
|
if envelope.timeout is not None:
|
|
493
493
|
arguments |= {"timeout": envelope.timeout}
|
|
494
494
|
return spec.request.model_validate_json(json.dumps(arguments, allow_nan=False))
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
def schema_contract() -> dict[str, dict[str, dict[str, Any]]]:
|
|
498
|
-
return {
|
|
499
|
-
scope: {
|
|
500
|
-
method: {
|
|
501
|
-
"request": spec.request.model_json_schema(),
|
|
502
|
-
"result": spec.response.json_schema(),
|
|
503
|
-
"mutation": spec.mutation,
|
|
504
|
-
}
|
|
505
|
-
for (entry_scope, method), spec in sorted(_OPERATIONS.items())
|
|
506
|
-
if entry_scope == scope
|
|
507
|
-
}
|
|
508
|
-
for scope in _ALL
|
|
509
|
-
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from collections.abc import Mapping
|
|
2
2
|
from ipaddress import IPv4Address
|
|
3
|
+
from re import sub
|
|
3
4
|
from typing import Annotated
|
|
4
5
|
|
|
5
6
|
from pydantic import (
|
|
@@ -33,12 +34,12 @@ class DataResponse[DataT](KleiModel):
|
|
|
33
34
|
|
|
34
35
|
class Player(KleiModel):
|
|
35
36
|
name: str
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
netid: str
|
|
38
|
+
prefab: Role | str
|
|
39
|
+
colour: str
|
|
40
|
+
eventlevel: int
|
|
40
41
|
|
|
41
|
-
@field_validator("
|
|
42
|
+
@field_validator("prefab", mode="before")
|
|
42
43
|
@classmethod
|
|
43
44
|
def preserve_custom_role(cls, value: object) -> object:
|
|
44
45
|
return Role(value) if isinstance(value, str) and value in Role else value
|
|
@@ -118,7 +119,9 @@ class Room(Lobby):
|
|
|
118
119
|
return ()
|
|
119
120
|
if not isinstance(value, str):
|
|
120
121
|
return value
|
|
121
|
-
players = parse_literal(
|
|
122
|
+
players = parse_literal(
|
|
123
|
+
sub(r"\A\s*return\b", "", value, count=1), "Klei room players"
|
|
124
|
+
)
|
|
122
125
|
if players == {}:
|
|
123
126
|
return ()
|
|
124
127
|
if not isinstance(players, list):
|
|
@@ -78,6 +78,14 @@ class _Metadata(FrozenModel):
|
|
|
78
78
|
msg = f"unsupported snapshot metadata format: {path}"
|
|
79
79
|
raise ValueError(msg)
|
|
80
80
|
value = parse_literal(source.removeprefix("return"), "snapshot metadata")
|
|
81
|
+
if isinstance(value, dict):
|
|
82
|
+
for name, model in (("clock", SnapshotClock), ("seasons", SnapshotSeasons)):
|
|
83
|
+
if isinstance(fields := value.get(name), dict):
|
|
84
|
+
value[name] = {
|
|
85
|
+
key: item
|
|
86
|
+
for key, item in fields.items()
|
|
87
|
+
if key in model.model_fields
|
|
88
|
+
}
|
|
81
89
|
return cls.model_validate(value)
|
|
82
90
|
|
|
83
91
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from .client import ClusterClient, ShardClient, Subscription, rpc_runtime
|
|
2
|
-
from .schema import
|
|
2
|
+
from .schema import load_schema
|
|
3
3
|
from .transport import (
|
|
4
4
|
INTERNAL_RPC_ADDRESS,
|
|
5
5
|
PUBLIC_RPC_SOCKET,
|
|
@@ -10,7 +10,6 @@ from .transport import (
|
|
|
10
10
|
__all__ = [
|
|
11
11
|
"INTERNAL_RPC_ADDRESS",
|
|
12
12
|
"PUBLIC_RPC_SOCKET",
|
|
13
|
-
"SCHEMA_FINGERPRINT",
|
|
14
13
|
"ClusterClient",
|
|
15
14
|
"ShardClient",
|
|
16
15
|
"Subscription",
|
|
@@ -29,7 +29,7 @@ from dst_server.timeouts import (
|
|
|
29
29
|
)
|
|
30
30
|
|
|
31
31
|
from .codec import ERROR, decode, decode_model, unwrap_outcome
|
|
32
|
-
from .schema import
|
|
32
|
+
from .schema import load_schema
|
|
33
33
|
|
|
34
34
|
type StreamKind = Literal["logs", "lifecycle", "events"]
|
|
35
35
|
capnp: Any = import_module("capnp")
|
|
@@ -178,10 +178,7 @@ class ClusterClient(RemoteEndpoint, ClusterAPI):
|
|
|
178
178
|
stream = await capnp.AsyncIoStream.create_unix_connection(fspath(path))
|
|
179
179
|
client = capnp.TwoPartyClient(stream)
|
|
180
180
|
response = (
|
|
181
|
-
await client
|
|
182
|
-
.bootstrap()
|
|
183
|
-
.cast_as(load_schema().Bootstrap)
|
|
184
|
-
.connect(schemaFingerprint=SCHEMA_FINGERPRINT)
|
|
181
|
+
await client.bootstrap().cast_as(load_schema().Bootstrap).connect()
|
|
185
182
|
)
|
|
186
183
|
return cls(stream, client, unwrap_outcome(response.result))
|
|
187
184
|
except BaseException:
|
|
@@ -34,10 +34,10 @@ interface Cluster extends(Endpoint) {
|
|
|
34
34
|
interface Agent extends(Endpoint) {}
|
|
35
35
|
|
|
36
36
|
interface Bootstrap {
|
|
37
|
-
connect @0 (
|
|
37
|
+
connect @0 () -> (result :Outcome(Cluster));
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
interface WorkerRegistry {
|
|
41
|
-
register @0 (
|
|
41
|
+
register @0 (agent :Agent) -> (result :Outcome(Unit));
|
|
42
42
|
failed @1 () -> (result :Outcome(Unit));
|
|
43
43
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from functools import cache
|
|
2
|
+
from importlib import import_module
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
capnp: Any = import_module("capnp")
|
|
7
|
+
RPC_SCHEMA_PATH = Path(__file__).with_name("schema") / "rpc.capnp"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@cache
|
|
11
|
+
def load_schema() -> Any:
|
|
12
|
+
return capnp.load(str(RPC_SCHEMA_PATH))
|
|
@@ -39,7 +39,7 @@ from dst_server.timeouts import (
|
|
|
39
39
|
from .client import RemoteEndpoint, StreamKind
|
|
40
40
|
from .client import Subscription as RemoteSubscription
|
|
41
41
|
from .codec import ERROR, encode, encode_model, failure, success
|
|
42
|
-
from .schema import
|
|
42
|
+
from .schema import load_schema
|
|
43
43
|
|
|
44
44
|
capnp: Any = import_module("capnp")
|
|
45
45
|
schema = load_schema()
|
|
@@ -343,15 +343,8 @@ class BootstrapServant(_Responder, schema.Bootstrap.Server):
|
|
|
343
343
|
super().__init__()
|
|
344
344
|
self.cluster = ClusterServant(controller, self._owner)
|
|
345
345
|
|
|
346
|
-
async def connect(self,
|
|
347
|
-
|
|
348
|
-
_context.results.result = failure(
|
|
349
|
-
ErrorInfo(
|
|
350
|
-
ErrorCode.INCOMPATIBLE_SCHEMA, ULID(), "RPC schema is incompatible"
|
|
351
|
-
)
|
|
352
|
-
)
|
|
353
|
-
else:
|
|
354
|
-
_context.results.result = success(self.cluster)
|
|
346
|
+
async def connect(self, _context: Any) -> None:
|
|
347
|
+
_context.results.result = success(self.cluster)
|
|
355
348
|
|
|
356
349
|
|
|
357
350
|
class RemoteAgent(RemoteEndpoint):
|
|
@@ -544,22 +537,7 @@ class WorkerRegistryServant(_Responder, schema.WorkerRegistry.Server):
|
|
|
544
537
|
self._closed = False
|
|
545
538
|
self._register_lock = asyncio.Lock()
|
|
546
539
|
|
|
547
|
-
async def register(
|
|
548
|
-
self,
|
|
549
|
-
schemaFingerprint: str,
|
|
550
|
-
agent: Any,
|
|
551
|
-
_context: Any,
|
|
552
|
-
) -> None:
|
|
553
|
-
if schemaFingerprint != SCHEMA_FINGERPRINT:
|
|
554
|
-
_context.results.result = failure(
|
|
555
|
-
ErrorInfo(
|
|
556
|
-
ErrorCode.INCOMPATIBLE_SCHEMA,
|
|
557
|
-
ULID(),
|
|
558
|
-
"RPC schema is incompatible",
|
|
559
|
-
)
|
|
560
|
-
)
|
|
561
|
-
return
|
|
562
|
-
|
|
540
|
+
async def register(self, agent: Any, _context: Any) -> None:
|
|
563
541
|
async def register() -> None:
|
|
564
542
|
async with self._register_lock:
|
|
565
543
|
if self._closed or self.remote is not None:
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
from functools import cache
|
|
3
|
-
from hashlib import sha256
|
|
4
|
-
from importlib import import_module
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from typing import Any
|
|
7
|
-
|
|
8
|
-
from pydantic import TypeAdapter
|
|
9
|
-
|
|
10
|
-
from dst_server.commands import schema_contract
|
|
11
|
-
from dst_server.errors import ErrorInfo
|
|
12
|
-
from dst_server.models.cluster import GameEventRecord, LifecycleRecord, LogRecord
|
|
13
|
-
|
|
14
|
-
capnp: Any = import_module("capnp")
|
|
15
|
-
RPC_SCHEMA_PATH = Path(__file__).with_name("schema") / "rpc.capnp"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def _payload_contract() -> bytes:
|
|
19
|
-
return json.dumps(
|
|
20
|
-
{
|
|
21
|
-
"wireFormat": 2,
|
|
22
|
-
"commands": schema_contract(),
|
|
23
|
-
"observations": {
|
|
24
|
-
model.__name__: model.model_json_schema()
|
|
25
|
-
for model in (LogRecord, LifecycleRecord, GameEventRecord)
|
|
26
|
-
},
|
|
27
|
-
"error": TypeAdapter(ErrorInfo).json_schema(),
|
|
28
|
-
},
|
|
29
|
-
sort_keys=True,
|
|
30
|
-
separators=(",", ":"),
|
|
31
|
-
).encode()
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
@cache
|
|
35
|
-
def load_schema() -> Any:
|
|
36
|
-
return capnp.load(str(RPC_SCHEMA_PATH))
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def _schema_fingerprint(payload: bytes) -> str:
|
|
40
|
-
return sha256(RPC_SCHEMA_PATH.read_bytes() + b"\0" + payload).hexdigest()
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
SCHEMA_FINGERPRINT = _schema_fingerprint(_payload_contract())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|