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.
Files changed (109) hide show
  1. {dst_server-0.2.1 → dst_server-0.2.2}/PKG-INFO +34 -15
  2. {dst_server-0.2.1 → dst_server-0.2.2}/README.md +33 -14
  3. {dst_server-0.2.1 → dst_server-0.2.2}/pyproject.toml +1 -1
  4. {dst_server-0.2.1 → dst_server-0.2.2}/pyproject.toml.orig +1 -1
  5. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/archive.py +26 -4
  6. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/cluster/daemon.py +2 -5
  7. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/commands.py +0 -15
  8. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/errors.py +0 -1
  9. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/klei/lobby.py +9 -6
  10. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/models/snapshot.py +8 -0
  11. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/rpc/__init__.py +1 -2
  12. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/rpc/client.py +2 -5
  13. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/rpc/schema/rpc.capnp +2 -2
  14. dst_server-0.2.2/src/dst_server/rpc/schema.py +12 -0
  15. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/rpc/servants.py +4 -26
  16. dst_server-0.2.1/src/dst_server/rpc/schema.py +0 -43
  17. {dst_server-0.2.1 → dst_server-0.2.2}/LICENSE +0 -0
  18. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/__init__.py +0 -0
  19. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/annotations/__init__.py +0 -0
  20. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/annotations/cli.py +0 -0
  21. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/annotations/generator.py +0 -0
  22. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/annotations/values.py +0 -0
  23. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/annotations/visitors.py +0 -0
  24. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/api.py +0 -0
  25. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/cluster/__init__.py +0 -0
  26. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/cluster/agent.py +0 -0
  27. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/cluster/cli.py +0 -0
  28. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/cluster/console.py +0 -0
  29. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/cluster/controller.py +0 -0
  30. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/cluster/service.py +0 -0
  31. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/cluster/subscriptions.py +0 -0
  32. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/concurrency.py +0 -0
  33. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/configuration/__init__.py +0 -0
  34. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/configuration/files.py +0 -0
  35. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/configuration/ini.py +0 -0
  36. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/configuration/models.py +0 -0
  37. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/configuration/overrides.py +0 -0
  38. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/configuration/presets.py +0 -0
  39. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/configuration/store.py +0 -0
  40. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/configuration/world.py +0 -0
  41. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/deployment/__init__.py +0 -0
  42. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/deployment/application.py +0 -0
  43. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/deployment/models.py +0 -0
  44. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/deployment/quadlet.py +0 -0
  45. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/events/__init__.py +0 -0
  46. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/events/base.py +0 -0
  47. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/events/player.py +0 -0
  48. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/events/record.py +0 -0
  49. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/events/server.py +0 -0
  50. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/events/world.py +0 -0
  51. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/game/__init__.py +0 -0
  52. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/game/client.py +0 -0
  53. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/game/enums.py +0 -0
  54. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/game/rpc.py +0 -0
  55. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/klei/__init__.py +0 -0
  56. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/klei/client.py +0 -0
  57. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/klei/enums.py +0 -0
  58. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/klei/schema.py +0 -0
  59. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/klei/version.py +0 -0
  60. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/klei_id.py +0 -0
  61. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/lua/dst_server/actions.lua +0 -0
  62. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/lua/dst_server/commands.lua +0 -0
  63. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/lua/dst_server/player_events.lua +0 -0
  64. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/lua/dst_server/player_queries.lua +0 -0
  65. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/lua/dst_server/player_values.lua +0 -0
  66. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/lua/dst_server/state.lua +0 -0
  67. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/lua/dst_server/telemetry.lua +0 -0
  68. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/lua/dst_server/values.lua +0 -0
  69. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/lua/dst_server/wire.lua +0 -0
  70. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/lua/dst_server/world_events.lua +0 -0
  71. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/lua/dst_server/world_queries.lua +0 -0
  72. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/lua/dst_server.lua +0 -0
  73. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/lua_codec.py +0 -0
  74. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/models/__init__.py +0 -0
  75. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/models/base.py +0 -0
  76. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/models/cluster.py +0 -0
  77. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/models/driver.py +0 -0
  78. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/models/entity.py +0 -0
  79. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/models/player.py +0 -0
  80. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/models/server.py +0 -0
  81. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/models/telemetry.py +0 -0
  82. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/mods/__init__.py +0 -0
  83. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/mods/files.py +0 -0
  84. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/mods/native.py +0 -0
  85. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/mods/process.py +0 -0
  86. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/mods/steamcmd.py +0 -0
  87. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/mods/workshop.py +0 -0
  88. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/netdata.py +0 -0
  89. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/py.typed +0 -0
  90. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/rpc/codec.py +0 -0
  91. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/rpc/transport.py +0 -0
  92. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/runtime/__init__.py +0 -0
  93. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/runtime/config.py +0 -0
  94. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/runtime/console.py +0 -0
  95. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/runtime/driver.py +0 -0
  96. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/runtime/fds.py +0 -0
  97. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/runtime/lifecycle.py +0 -0
  98. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/runtime/operational.py +0 -0
  99. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/runtime/request.py +0 -0
  100. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/runtime/server.py +0 -0
  101. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/runtime/supervisor.py +0 -0
  102. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/telemetry/__init__.py +0 -0
  103. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/telemetry/config.py +0 -0
  104. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/telemetry/exporter.py +0 -0
  105. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/telemetry/otel.py +0 -0
  106. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/telemetry/outbox.py +0 -0
  107. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/telemetry/recorder.py +0 -0
  108. {dst_server-0.2.1 → dst_server-0.2.2}/src/dst_server/telemetry/stream.py +0 -0
  109. {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.1
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; connections check a schema fingerprint, so the client and daemon should use matching versions. |
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
- They reject unknown fields, wrong types, and dynamic expressions.
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
- Before uploading to R2, set [obstore's S3 environment variables](https://developmentseed.org/obstore/latest/api/store/aws/#obstore.store.S3Config) in the calling process:
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
- S3 configuration still uses the AWS environment variables above; there are no environment variables for these prefixes.
875
- `S3Store(region="auto")` uses [R2's `auto` region](https://developers.cloudflare.com/r2/api/s3/api/#bucket-region), so `AWS_REGION` is unnecessary.
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; connections check a schema fingerprint, so the client and daemon should use matching versions. |
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
- They reject unknown fields, wrong types, and dynamic expressions.
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
- Before uploading to R2, set [obstore's S3 environment variables](https://developmentseed.org/obstore/latest/api/store/aws/#obstore.store.S3Config) in the calling process:
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
- S3 configuration still uses the AWS environment variables above; there are no environment variables for these prefixes.
852
- `S3Store(region="auto")` uses [R2's `auto` region](https://developers.cloudflare.com/r2/api/s3/api/#bucket-region), so `AWS_REGION` is unnecessary.
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/).
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dst-server"
3
- version = "0.2.1"
3
+ version = "0.2.2"
4
4
  description = "Start and control a Don't Starve Together server process."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dst-server"
3
- version = "0.2.1"
3
+ version = "0.2.2"
4
4
  description = "Start and control a Don't Starve Together server process."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -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 using AWS environment variables and literal key/URL prefixes."""
49
+ """Upload with explicit S3 settings overriding AWS environment variables."""
44
50
  from obstore.store import S3Store
45
51
 
46
- store = S3Store(region="auto")
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 SCHEMA_FINGERPRINT, load_schema
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
- }
@@ -16,7 +16,6 @@ class ErrorCode(StrEnum):
16
16
  TIMEOUT = "timeout"
17
17
  OVERFLOW = "overflow"
18
18
  TOPOLOGY_CHANGE_REQUIRED = "topologyChangeRequired"
19
- INCOMPATIBLE_SCHEMA = "incompatibleSchema"
20
19
  INTERNAL = "internal"
21
20
  INDETERMINATE = "indeterminate"
22
21
 
@@ -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
- kuid: str
37
- role: Role | str | None = None
38
- steam_id: int | None = None
39
- ip: IPv4Address | None = None
37
+ netid: str
38
+ prefab: Role | str
39
+ colour: str
40
+ eventlevel: int
40
41
 
41
- @field_validator("role", mode="before")
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(value, "Klei room players")
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 SCHEMA_FINGERPRINT, load_schema
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 SCHEMA_FINGERPRINT, load_schema
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 (schemaFingerprint :Text) -> (result :Outcome(Cluster));
37
+ connect @0 () -> (result :Outcome(Cluster));
38
38
  }
39
39
 
40
40
  interface WorkerRegistry {
41
- register @0 (schemaFingerprint :Text, agent :Agent) -> (result :Outcome(Unit));
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 SCHEMA_FINGERPRINT, load_schema
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, schemaFingerprint: str, _context: Any) -> None:
347
- if schemaFingerprint != SCHEMA_FINGERPRINT:
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