onlyworlds 0.1.0__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 (104) hide show
  1. onlyworlds-0.1.0/.gitattributes +3 -0
  2. onlyworlds-0.1.0/.github/workflows/ci.yml +35 -0
  3. onlyworlds-0.1.0/.gitignore +6 -0
  4. onlyworlds-0.1.0/CHANGELOG.md +12 -0
  5. onlyworlds-0.1.0/LICENSE +21 -0
  6. onlyworlds-0.1.0/PKG-INFO +122 -0
  7. onlyworlds-0.1.0/README.md +111 -0
  8. onlyworlds-0.1.0/codegen/generate_schema.py +201 -0
  9. onlyworlds-0.1.0/codegen/schema-dist/.gitattributes +1 -0
  10. onlyworlds-0.1.0/codegen/schema-dist/LICENSE +21 -0
  11. onlyworlds-0.1.0/codegen/schema-dist/MANIFEST.json +73 -0
  12. onlyworlds-0.1.0/codegen/schema-dist/README.md +88 -0
  13. onlyworlds-0.1.0/codegen/schema-dist/VERSION +3 -0
  14. onlyworlds-0.1.0/codegen/schema-dist/example/hyperion/elements/ability/piano-playing--60195ff8.json +31 -0
  15. onlyworlds-0.1.0/codegen/schema-dist/example/hyperion/elements/character/mr-steinway--678bd862.json +46 -0
  16. onlyworlds-0.1.0/codegen/schema-dist/example/hyperion/elements/character/rachmaninov--4c0d52b4.json +49 -0
  17. onlyworlds-0.1.0/codegen/schema-dist/example/hyperion/elements/character/the-consul--8b711e25.json +56 -0
  18. onlyworlds-0.1.0/codegen/schema-dist/example/hyperion/elements/collective/pack-of-bellowing-saurians--1218fc88.json +32 -0
  19. onlyworlds-0.1.0/codegen/schema-dist/example/hyperion/elements/construct/hegemony-standard--e40d0001.json +59 -0
  20. onlyworlds-0.1.0/codegen/schema-dist/example/hyperion/elements/construct/rachmaninov-s-prelude--6ebf1c1e.json +53 -0
  21. onlyworlds-0.1.0/codegen/schema-dist/example/hyperion/elements/construct/steinway-piano--d0a334b1.json +39 -0
  22. onlyworlds-0.1.0/codegen/schema-dist/example/hyperion/elements/creature/great-green-saurian--8406227d.json +35 -0
  23. onlyworlds-0.1.0/codegen/schema-dist/example/hyperion/elements/creature/greater-green-saurian--e79a4c7c.json +35 -0
  24. onlyworlds-0.1.0/codegen/schema-dist/example/hyperion/elements/event/the-primal-performance--8b461fb6.json +45 -0
  25. onlyworlds-0.1.0/codegen/schema-dist/example/hyperion/elements/event/the-raging-of-the-beasts--95bb5602.json +39 -0
  26. onlyworlds-0.1.0/codegen/schema-dist/example/hyperion/elements/institution/the-hegemony--b6872c95.json +26 -0
  27. onlyworlds-0.1.0/codegen/schema-dist/example/hyperion/elements/language/western-musical-notation--f856e44c.json +21 -0
  28. onlyworlds-0.1.0/codegen/schema-dist/example/hyperion/elements/location/unnamed-planet--6283dd71.json +44 -0
  29. onlyworlds-0.1.0/codegen/schema-dist/example/hyperion/elements/map/map-of-unnamed-planet--153cd291.json +20 -0
  30. onlyworlds-0.1.0/codegen/schema-dist/example/hyperion/elements/marker/swamps-point-0--f42a6e15.json +19 -0
  31. onlyworlds-0.1.0/codegen/schema-dist/example/hyperion/elements/marker/swamps-point-1--3f8fe28c.json +19 -0
  32. onlyworlds-0.1.0/codegen/schema-dist/example/hyperion/elements/marker/swamps-point-2--811d2c15.json +19 -0
  33. onlyworlds-0.1.0/codegen/schema-dist/example/hyperion/elements/marker/swamps-point-3--ea668c31.json +19 -0
  34. onlyworlds-0.1.0/codegen/schema-dist/example/hyperion/elements/marker/swamps-point-4--4e843a51.json +19 -0
  35. onlyworlds-0.1.0/codegen/schema-dist/example/hyperion/elements/marker/swamps-point-5--48b3cfa2.json +19 -0
  36. onlyworlds-0.1.0/codegen/schema-dist/example/hyperion/elements/narrative/chapter-1--39fd9536.json +41 -0
  37. onlyworlds-0.1.0/codegen/schema-dist/example/hyperion/elements/narrative/hyperion--bb17eaba.json +41 -0
  38. onlyworlds-0.1.0/codegen/schema-dist/example/hyperion/elements/narrative/new-book--08afd034.json +41 -0
  39. onlyworlds-0.1.0/codegen/schema-dist/example/hyperion/elements/narrative/new-page--2f7bc2f5.json +41 -0
  40. onlyworlds-0.1.0/codegen/schema-dist/example/hyperion/elements/narrative/story-of-the-old-piano--2a6553ca.json +45 -0
  41. onlyworlds-0.1.0/codegen/schema-dist/example/hyperion/elements/object/ebony-spaceship--f4e8e286.json +30 -0
  42. onlyworlds-0.1.0/codegen/schema-dist/example/hyperion/elements/object/spaceship-balcony--dd75923a.json +30 -0
  43. onlyworlds-0.1.0/codegen/schema-dist/example/hyperion/elements/object/the-consul-s-steinway--75bfd994.json +30 -0
  44. onlyworlds-0.1.0/codegen/schema-dist/example/hyperion/elements/phenomenon/piano-music--5be90c7d.json +23 -0
  45. onlyworlds-0.1.0/codegen/schema-dist/example/hyperion/elements/pin/the-consul-s-balcony-seat--df789513.json +19 -0
  46. onlyworlds-0.1.0/codegen/schema-dist/example/hyperion/elements/species/saurians--d24e2d1e.json +33 -0
  47. onlyworlds-0.1.0/codegen/schema-dist/example/hyperion/elements/title/consul-of-the-hegemony--241a68ab.json +39 -0
  48. onlyworlds-0.1.0/codegen/schema-dist/example/hyperion/elements/trait/swamp-rage--dff0708a.json +27 -0
  49. onlyworlds-0.1.0/codegen/schema-dist/example/hyperion/elements/zone/swamps--d138f887.json +27 -0
  50. onlyworlds-0.1.0/codegen/schema-dist/example/hyperion/world.json +20 -0
  51. onlyworlds-0.1.0/codegen/schema-dist/presentation.json +129 -0
  52. onlyworlds-0.1.0/codegen/schema-dist/schema/ability.yaml +75 -0
  53. onlyworlds-0.1.0/codegen/schema-dist/schema/base_properties.yaml +26 -0
  54. onlyworlds-0.1.0/codegen/schema-dist/schema/character.yaml +168 -0
  55. onlyworlds-0.1.0/codegen/schema-dist/schema/collective.yaml +80 -0
  56. onlyworlds-0.1.0/codegen/schema-dist/schema/construct.yaml +145 -0
  57. onlyworlds-0.1.0/codegen/schema-dist/schema/creature.yaml +92 -0
  58. onlyworlds-0.1.0/codegen/schema-dist/schema/event.yaml +128 -0
  59. onlyworlds-0.1.0/codegen/schema-dist/schema/family.yaml +74 -0
  60. onlyworlds-0.1.0/codegen/schema-dist/schema/institution.yaml +65 -0
  61. onlyworlds-0.1.0/codegen/schema-dist/schema/language.yaml +42 -0
  62. onlyworlds-0.1.0/codegen/schema-dist/schema/law.yaml +64 -0
  63. onlyworlds-0.1.0/codegen/schema-dist/schema/location.yaml +178 -0
  64. onlyworlds-0.1.0/codegen/schema-dist/schema/map.yaml +30 -0
  65. onlyworlds-0.1.0/codegen/schema-dist/schema/marker.yaml +29 -0
  66. onlyworlds-0.1.0/codegen/schema-dist/schema/narrative.yaml +156 -0
  67. onlyworlds-0.1.0/codegen/schema-dist/schema/object.yaml +78 -0
  68. onlyworlds-0.1.0/codegen/schema-dist/schema/phenomenon.yaml +57 -0
  69. onlyworlds-0.1.0/codegen/schema-dist/schema/pin.yaml +26 -0
  70. onlyworlds-0.1.0/codegen/schema-dist/schema/relation.yaml +133 -0
  71. onlyworlds-0.1.0/codegen/schema-dist/schema/species.yaml +90 -0
  72. onlyworlds-0.1.0/codegen/schema-dist/schema/title.yaml +132 -0
  73. onlyworlds-0.1.0/codegen/schema-dist/schema/trait.yaml +74 -0
  74. onlyworlds-0.1.0/codegen/schema-dist/schema/world.yaml +71 -0
  75. onlyworlds-0.1.0/codegen/schema-dist/schema/zone.yaml +53 -0
  76. onlyworlds-0.1.0/codegen/schema-dist/schema.json +2901 -0
  77. onlyworlds-0.1.0/codegen/schema-dist/walk/rulings.yaml +213 -0
  78. onlyworlds-0.1.0/codegen/schema-dist/walk/schema_walk.py +321 -0
  79. onlyworlds-0.1.0/codegen/schema-pin.json +13 -0
  80. onlyworlds-0.1.0/pyproject.toml +46 -0
  81. onlyworlds-0.1.0/src/onlyworlds/__init__.py +79 -0
  82. onlyworlds-0.1.0/src/onlyworlds/_ids.py +28 -0
  83. onlyworlds-0.1.0/src/onlyworlds/_jsonfmt.py +135 -0
  84. onlyworlds-0.1.0/src/onlyworlds/_schema.py +556 -0
  85. onlyworlds-0.1.0/src/onlyworlds/_version.py +1 -0
  86. onlyworlds-0.1.0/src/onlyworlds/errors.py +133 -0
  87. onlyworlds-0.1.0/src/onlyworlds/export.py +41 -0
  88. onlyworlds-0.1.0/src/onlyworlds/folder.py +413 -0
  89. onlyworlds-0.1.0/src/onlyworlds/http.py +606 -0
  90. onlyworlds-0.1.0/src/onlyworlds/push.py +375 -0
  91. onlyworlds-0.1.0/src/onlyworlds/py.typed +0 -0
  92. onlyworlds-0.1.0/tests/conftest.py +72 -0
  93. onlyworlds-0.1.0/tests/test_client.py +360 -0
  94. onlyworlds-0.1.0/tests/test_export.py +40 -0
  95. onlyworlds-0.1.0/tests/test_filenames.py +67 -0
  96. onlyworlds-0.1.0/tests/test_jsonfmt.py +76 -0
  97. onlyworlds-0.1.0/tests/test_live.py +42 -0
  98. onlyworlds-0.1.0/tests/test_push.py +297 -0
  99. onlyworlds-0.1.0/tests/test_reader.py +119 -0
  100. onlyworlds-0.1.0/tests/test_schema.py +56 -0
  101. onlyworlds-0.1.0/tests/test_staging_writes.py +264 -0
  102. onlyworlds-0.1.0/tests/test_wire.py +167 -0
  103. onlyworlds-0.1.0/tests/test_writer.py +143 -0
  104. onlyworlds-0.1.0/uv.lock +418 -0
@@ -0,0 +1,3 @@
1
+ # One line ending everywhere, so a Windows checkout does not turn the generated
2
+ # src/onlyworlds/_schema.py into CRLF (codegen --check compares it byte for byte).
3
+ * text=auto eol=lf
@@ -0,0 +1,35 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ gates:
10
+ strategy:
11
+ fail-fast: false
12
+ matrix:
13
+ os: [ubuntu-latest, windows-latest]
14
+ runs-on: ${{ matrix.os }}
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: astral-sh/setup-uv@v5
18
+ - run: uv sync
19
+ - run: uv run pytest -q
20
+ - run: uv run ruff check .
21
+ - run: uv run mypy --strict src
22
+ - run: uv run python codegen/generate_schema.py --check
23
+
24
+ wire:
25
+ # keel's own OpenAPI document and the public demo world, read only. Red here with
26
+ # the gates above green means the server moved under the client, not that a change broke it.
27
+ runs-on: ubuntu-latest
28
+ env:
29
+ OW_OPENAPI: https://www.onlyworlds.com/api/v2/openapi.json
30
+ OW_LIVE: "1"
31
+ steps:
32
+ - uses: actions/checkout@v4
33
+ - uses: astral-sh/setup-uv@v5
34
+ - run: uv sync
35
+ - run: uv run pytest -q tests/test_wire.py tests/test_live.py
@@ -0,0 +1,6 @@
1
+ .venv/
2
+ __pycache__/
3
+ .pytest_cache/
4
+ .mypy_cache/
5
+ .ruff_cache/
6
+ dist/
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+
3
+ The package version runs on its own semver; the schema it is generated from is `SCHEMA_DIST_TAG`.
4
+
5
+ ## 0.1.0 (2026-10-10)
6
+
7
+ The first release on PyPI. Python 3.12+, no runtime dependencies.
8
+
9
+ - **The v2 API client** (`Client`): read, write, link operations, bulk, the change feed (`ChangeWalk`), and the error envelope as `ApiError`. It retries busy and gateway answers (`429`, `502`, `503`, `504`) and honours `Retry-After` (`RetryPolicy`). It never retries a wrong PIN, since each retry counts toward locking the key. The transport is swappable (`Transport`).
10
+ - **World folders** (format v0.3.7): `read_folder` and `write_folder`, filenames that match Atlas's, and the World's three unit settings (`length_unit`, `mass_unit`, `distance_unit`) carried like any world key. Reading never changes a file.
11
+ - **Export and push:** `export_world` writes an online world to a folder. `push` sends a folder's edits back as the difference from a copy exported before editing (`plan_push` to preview it, a log to resume it). A run stops at the first answer that refuses the key itself. What push never sends is listed in `NOT_SENT`.
12
+ - **The schema**, generated from schema-dist `v0.31.0-dist.19` (canonical `00.31.00`): `ELEMENT_TYPES`, `FIELD_KINDS`, and the pinned manifest's hash.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-2025 OnlyWorlds
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,122 @@
1
+ Metadata-Version: 2.5
2
+ Name: onlyworlds
3
+ Version: 0.1.0
4
+ Summary: OnlyWorlds for Python: the world folder format (read, write, push) and a client for the v2 API.
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Typing :: Typed
9
+ Requires-Python: >=3.12
10
+ Description-Content-Type: text/markdown
11
+
12
+ # onlyworlds (Python)
13
+
14
+ OnlyWorlds is an open standard for worldbuilding data: 22 element types (characters, locations, events, laws and the rest) that any tool can read and write. This is a Python client for its API, and a reader and writer for OnlyWorlds world folders (a world as a folder of JSON files). Python 3.12+, no runtime dependencies.
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ pip install onlyworlds
20
+ ```
21
+
22
+ The newest unreleased code: `pip install git+https://github.com/OnlyWorlds/python-sdk`.
23
+
24
+ ## First run
25
+
26
+ Moppetopia is a public demo world. Its read-only key, `0000000001`, needs no account:
27
+
28
+ ```python
29
+ from onlyworlds import Client
30
+
31
+ client = Client("0000000001")
32
+ print(client.get_world()["name"])
33
+ for c in client.list_page("character", filter={"name__icontains": "fluffington"})["data"]:
34
+ print(" ", c["name"])
35
+ ```
36
+
37
+ ```
38
+ Moppetopia
39
+ Admiral Fluffington
40
+ ```
41
+
42
+ ## Your own world
43
+
44
+ Create a world at [onlyworlds.com](https://www.onlyworlds.com); its keys are on the world's page. A new key is shown once, so copy it then.
45
+
46
+ - An `ow_r_` key reads, with no PIN: `Client("ow_r_…")`.
47
+ - An `ow_w_` key reads and writes. Writes also send a PIN: `Client("ow_w_…", "…")`.
48
+ - For code that writes, give it its own [agent seat](https://onlyworlds.github.io/docs/development/agents), a key for one tool or script. The seat's key and its `ow_s_` secret (passed as the PIN) work in one world, and you can remove them without touching your account PIN.
49
+
50
+ Elements are plain dicts:
51
+
52
+ ```python
53
+ writer = Client("ow_w_…", "ow_s_…")
54
+ peak = writer.create("location", {"name": "Dragon Peak"})
55
+ writer.patch("location", peak["id"], {"supertype": "Mountain"})
56
+
57
+ for character in client.iter_elements("character", expand=["species"]):
58
+ print(character["name"])
59
+ ```
60
+
61
+ ## World folders
62
+
63
+ A world folder is one JSON file per element, in [the world folder format](https://onlyworlds.github.io/docs/schema/folders). [Atlas](https://atlas.onlyworlds.com), the OnlyWorlds editor, reads and writes the same format.
64
+
65
+ ```python
66
+ from onlyworlds import export_world, read_folder
67
+
68
+ export_world("moppetopia", api_key="0000000001")
69
+ folder = read_folder("moppetopia")
70
+ print(folder.world["name"], len(folder.elements), "elements")
71
+ ```
72
+
73
+ `write_folder(root, world, elements)` writes a folder from a world dict and a dict of element lists by type. Reading never changes a file, and a file without an id is skipped and left alone.
74
+
75
+ To send a folder's edits back, keep a copy exported before you edited, then push the difference:
76
+
77
+ ```python
78
+ from onlyworlds import push
79
+
80
+ result = push("moppetopia-before", "moppetopia", api_key="ow_w_…", pin="ow_s_…", log_path="push.jsonl")
81
+ ```
82
+
83
+ `push` sends only the fields that changed, retries what the server asks it to retry, checks that what came back is what it sent, and logs every write, so running it again skips what already landed. `dry_run=True` shows the plan without sending it.
84
+
85
+ ## Errors
86
+
87
+ A failed call raises `ApiError` with the API's `code`, `param` (the field that failed) and `doc_url`, and flags for the cases you handle differently: `is_validation_error`, `is_auth_error`, `is_not_author`, `is_owner_only`, `is_id_conflict`, `is_resync_required`, `is_busy`.
88
+
89
+ ## The change feed
90
+
91
+ ```python
92
+ walk = client.walk_changes(saved_cursor) # None for everything
93
+ for op in walk:
94
+ ... # op["op"] is "upsert" or "delete"
95
+ saved_cursor = walk.cursor # opaque: keep it, don't parse it
96
+ ```
97
+
98
+ ## Schema
99
+
100
+ The 22 element types and each field's kind are generated from the published [schema distribution](https://github.com/OnlyWorlds/schema-dist); `onlyworlds.SCHEMA_VERSION` and `onlyworlds.SCHEMA_DIST_TAG` say which one. The package's own version doesn't track the schema's.
101
+
102
+ Not yet: typed element models, the account routes, a snapshot writer.
103
+
104
+ ## Develop
105
+
106
+ ```bash
107
+ uv sync
108
+ uv run pytest -q
109
+ uv run ruff check . && uv run mypy --strict src
110
+ uv run python codegen/generate_schema.py --check
111
+ ```
112
+
113
+ Three groups of tests run only when asked:
114
+
115
+ - `OW_CONFORMANCE_FIXTURE=<path>`: the folder-format conformance fixture from the Atlas repo.
116
+ - `OW_OPENAPI=https://www.onlyworlds.com/api/v2/openapi.json` (or a saved copy): checks the client's routes, parameters and fields against the API's own OpenAPI document.
117
+ - `OW_LIVE=1`: read-only calls against the demo world.
118
+
119
+
120
+ ## License
121
+
122
+ MIT. See `LICENSE`.
@@ -0,0 +1,111 @@
1
+ # onlyworlds (Python)
2
+
3
+ OnlyWorlds is an open standard for worldbuilding data: 22 element types (characters, locations, events, laws and the rest) that any tool can read and write. This is a Python client for its API, and a reader and writer for OnlyWorlds world folders (a world as a folder of JSON files). Python 3.12+, no runtime dependencies.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install onlyworlds
9
+ ```
10
+
11
+ The newest unreleased code: `pip install git+https://github.com/OnlyWorlds/python-sdk`.
12
+
13
+ ## First run
14
+
15
+ Moppetopia is a public demo world. Its read-only key, `0000000001`, needs no account:
16
+
17
+ ```python
18
+ from onlyworlds import Client
19
+
20
+ client = Client("0000000001")
21
+ print(client.get_world()["name"])
22
+ for c in client.list_page("character", filter={"name__icontains": "fluffington"})["data"]:
23
+ print(" ", c["name"])
24
+ ```
25
+
26
+ ```
27
+ Moppetopia
28
+ Admiral Fluffington
29
+ ```
30
+
31
+ ## Your own world
32
+
33
+ Create a world at [onlyworlds.com](https://www.onlyworlds.com); its keys are on the world's page. A new key is shown once, so copy it then.
34
+
35
+ - An `ow_r_` key reads, with no PIN: `Client("ow_r_…")`.
36
+ - An `ow_w_` key reads and writes. Writes also send a PIN: `Client("ow_w_…", "…")`.
37
+ - For code that writes, give it its own [agent seat](https://onlyworlds.github.io/docs/development/agents), a key for one tool or script. The seat's key and its `ow_s_` secret (passed as the PIN) work in one world, and you can remove them without touching your account PIN.
38
+
39
+ Elements are plain dicts:
40
+
41
+ ```python
42
+ writer = Client("ow_w_…", "ow_s_…")
43
+ peak = writer.create("location", {"name": "Dragon Peak"})
44
+ writer.patch("location", peak["id"], {"supertype": "Mountain"})
45
+
46
+ for character in client.iter_elements("character", expand=["species"]):
47
+ print(character["name"])
48
+ ```
49
+
50
+ ## World folders
51
+
52
+ A world folder is one JSON file per element, in [the world folder format](https://onlyworlds.github.io/docs/schema/folders). [Atlas](https://atlas.onlyworlds.com), the OnlyWorlds editor, reads and writes the same format.
53
+
54
+ ```python
55
+ from onlyworlds import export_world, read_folder
56
+
57
+ export_world("moppetopia", api_key="0000000001")
58
+ folder = read_folder("moppetopia")
59
+ print(folder.world["name"], len(folder.elements), "elements")
60
+ ```
61
+
62
+ `write_folder(root, world, elements)` writes a folder from a world dict and a dict of element lists by type. Reading never changes a file, and a file without an id is skipped and left alone.
63
+
64
+ To send a folder's edits back, keep a copy exported before you edited, then push the difference:
65
+
66
+ ```python
67
+ from onlyworlds import push
68
+
69
+ result = push("moppetopia-before", "moppetopia", api_key="ow_w_…", pin="ow_s_…", log_path="push.jsonl")
70
+ ```
71
+
72
+ `push` sends only the fields that changed, retries what the server asks it to retry, checks that what came back is what it sent, and logs every write, so running it again skips what already landed. `dry_run=True` shows the plan without sending it.
73
+
74
+ ## Errors
75
+
76
+ A failed call raises `ApiError` with the API's `code`, `param` (the field that failed) and `doc_url`, and flags for the cases you handle differently: `is_validation_error`, `is_auth_error`, `is_not_author`, `is_owner_only`, `is_id_conflict`, `is_resync_required`, `is_busy`.
77
+
78
+ ## The change feed
79
+
80
+ ```python
81
+ walk = client.walk_changes(saved_cursor) # None for everything
82
+ for op in walk:
83
+ ... # op["op"] is "upsert" or "delete"
84
+ saved_cursor = walk.cursor # opaque: keep it, don't parse it
85
+ ```
86
+
87
+ ## Schema
88
+
89
+ The 22 element types and each field's kind are generated from the published [schema distribution](https://github.com/OnlyWorlds/schema-dist); `onlyworlds.SCHEMA_VERSION` and `onlyworlds.SCHEMA_DIST_TAG` say which one. The package's own version doesn't track the schema's.
90
+
91
+ Not yet: typed element models, the account routes, a snapshot writer.
92
+
93
+ ## Develop
94
+
95
+ ```bash
96
+ uv sync
97
+ uv run pytest -q
98
+ uv run ruff check . && uv run mypy --strict src
99
+ uv run python codegen/generate_schema.py --check
100
+ ```
101
+
102
+ Three groups of tests run only when asked:
103
+
104
+ - `OW_CONFORMANCE_FIXTURE=<path>`: the folder-format conformance fixture from the Atlas repo.
105
+ - `OW_OPENAPI=https://www.onlyworlds.com/api/v2/openapi.json` (or a saved copy): checks the client's routes, parameters and fields against the API's own OpenAPI document.
106
+ - `OW_LIVE=1`: read-only calls against the demo world.
107
+
108
+
109
+ ## License
110
+
111
+ MIT. See `LICENSE`.
@@ -0,0 +1,201 @@
1
+ #!/usr/bin/env python3
2
+ """Generate src/onlyworlds/_schema.py from the vendored schema distribution.
3
+
4
+ Build-time only: needs PyYAML (the walk imports it). The generated module has no
5
+ dependencies, so the published package does not need PyYAML at runtime.
6
+
7
+ The type list comes from THE walk (codegen/schema-dist/walk/schema_walk.py),
8
+ imported, never re-implemented. A second witness is checked on every run: the
9
+ walk's list must equal the dist's schema/*.yaml stems minus the two non-element
10
+ files (base_properties, world). If they ever disagree, generation fails instead
11
+ of picking one.
12
+
13
+ Order of checks matters: the dist is authenticated (MANIFEST.json against the
14
+ pin) and its contents verified (every file against MANIFEST.json) BEFORE the
15
+ walk is imported, because importing unverified code to answer a question about
16
+ the verified tree would be the complicit-manifest case.
17
+
18
+ Usage:
19
+ python codegen/generate_schema.py write the module
20
+ python codegen/generate_schema.py --check exit 1 if the committed module is stale
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import argparse
26
+ import hashlib
27
+ import importlib.util
28
+ import json
29
+ import sys
30
+ from pathlib import Path
31
+ from typing import Any
32
+
33
+ HERE = Path(__file__).resolve().parent
34
+ DIST = HERE / "schema-dist"
35
+ PIN = HERE / "schema-pin.json"
36
+ OUT = HERE.parent / "src" / "onlyworlds" / "_schema.py"
37
+
38
+ NON_ELEMENT_SCHEMA_FILES = {"base_properties", "world"}
39
+
40
+
41
+ def _sha256(path: Path) -> str:
42
+ return hashlib.sha256(path.read_bytes()).hexdigest()
43
+
44
+
45
+ def verify_dist() -> dict[str, object]:
46
+ """Authenticate the vendored tree. Returns the pin record. Raises SystemExit on failure."""
47
+ pin: dict[str, object] = json.loads(PIN.read_text(encoding="utf-8"))
48
+ manifest = DIST / "MANIFEST.json"
49
+ got = _sha256(manifest)
50
+ if got != pin["manifest_sha256"]:
51
+ raise SystemExit(
52
+ f"schema-dist MANIFEST.json sha256 {got} != pinned {pin['manifest_sha256']} "
53
+ "(moved tag, or a re-vendor without a re-pin)"
54
+ )
55
+ files: dict[str, str] = json.loads(manifest.read_text(encoding="utf-8"))["files"]
56
+ bad = [f for f, h in sorted(files.items()) if not (DIST / f).is_file() or _sha256(DIST / f) != h]
57
+ if bad:
58
+ raise SystemExit(f"schema-dist files do not match MANIFEST.json: {bad}")
59
+ version = dict(
60
+ line.split(": ", 1) for line in (DIST / "VERSION").read_text(encoding="utf-8").splitlines() if ": " in line
61
+ )
62
+ if version.get("canonical") != pin["canonical_version"] or int(version.get("serial", -1)) != pin["dist_serial"]:
63
+ raise SystemExit(f"schema-dist VERSION {version} disagrees with the pin record")
64
+ return pin
65
+
66
+
67
+ # The walk's kinds -> the wire vocabulary the npm SDK's FIELD_SCHEMA uses.
68
+ KIND_NAMES = {"scalar_str": "text", "scalar_int": "integer", "single": "single_link", "multi": "multi_link"}
69
+ # Notes the walk is known to emit on this pin. Any OTHER note fails generation
70
+ # (rulings.yaml: unknown-field-types-must-be-surfaced): silence is how a newer
71
+ # schema meets an older decoder and drops a field.
72
+ EXPECTED_NOTES = ("pin.element: `generic-link`", "relation.events: field declared more than once")
73
+
74
+
75
+ def load_walk() -> Any:
76
+ spec = importlib.util.spec_from_file_location("schema_walk", DIST / "walk" / "schema_walk.py")
77
+ assert spec is not None and spec.loader is not None
78
+ walk = importlib.util.module_from_spec(spec)
79
+ spec.loader.exec_module(walk)
80
+ return walk
81
+
82
+
83
+ def field_kinds(walk: Any, types: list[str]) -> dict[str, dict[str, str]]:
84
+ """type -> {wire field name: kind}. Base fields by a DECLARED mapping (the SDK's):
85
+ base_properties.yaml names lowercased; `Id` and `World` dropped (identity, and a
86
+ field the API rejects in bodies). The generic link (pin.element) is served as
87
+ the pair element_type (text) + element_id (single_link), as the SDK emits it."""
88
+ schema = DIST / "schema"
89
+ base_doc = walk.load_yaml(schema, "base_properties")
90
+ base = {
91
+ name.lower(): "text"
92
+ for name, spec in base_doc["properties"].items()
93
+ if name.lower() not in ("id", "world") and spec.get("type") == "string"
94
+ }
95
+ notes: list[str] = []
96
+ out: dict[str, dict[str, str]] = {}
97
+ for t in types:
98
+ kinds = dict(base)
99
+ for f in walk.flatten_fields(walk.load_yaml(schema, t), t, note=notes.append):
100
+ if f["kind"] == "generic":
101
+ kinds[f"{f['name']}_type"] = "text"
102
+ kinds[f"{f['name']}_id"] = "single_link"
103
+ elif f["kind"] in KIND_NAMES:
104
+ kinds[f["name"]] = KIND_NAMES[f["kind"]]
105
+ else:
106
+ raise SystemExit(f"{t}.{f['name']}: unknown kind {f['kind']!r} from the walk")
107
+ out[t] = kinds
108
+ unexpected = [n for n in notes if not n.startswith(EXPECTED_NOTES)]
109
+ if unexpected:
110
+ raise SystemExit(f"the walk raised notes this generator does not know: {unexpected}")
111
+ return out
112
+
113
+
114
+ def generic_pairs(walk: Any, types: list[str]) -> dict[str, list[tuple[str, str]]]:
115
+ """type -> [(type column, id column)] for each generic link: keel refuses a write that sets one half."""
116
+ schema = DIST / "schema"
117
+ out: dict[str, list[tuple[str, str]]] = {}
118
+ for t in types:
119
+ pairs = [
120
+ (f"{f['name']}_type", f"{f['name']}_id")
121
+ for f in walk.flatten_fields(walk.load_yaml(schema, t), t, note=lambda _n: None)
122
+ if f["kind"] == "generic"
123
+ ]
124
+ if pairs:
125
+ out[t] = pairs
126
+ return out
127
+
128
+
129
+ def element_types(walk: Any) -> list[str]:
130
+ from_walk: list[str] = list(walk.ELEMENT_TYPES)
131
+ from_files = sorted(p.stem for p in (DIST / "schema").glob("*.yaml") if p.stem not in NON_ELEMENT_SCHEMA_FILES)
132
+ if sorted(from_walk) != from_files or len(set(from_walk)) != len(from_walk):
133
+ raise SystemExit(f"the walk's ELEMENT_TYPES {from_walk} disagree with the dist's schema files {from_files}")
134
+ return from_walk
135
+
136
+
137
+ def render(
138
+ pin: dict[str, object],
139
+ types: list[str],
140
+ kinds: dict[str, dict[str, str]],
141
+ pairs: dict[str, list[tuple[str, str]]],
142
+ ) -> str:
143
+ lines = [
144
+ "# GENERATED by codegen/generate_schema.py from the vendored schema distribution. Do not hand-edit.",
145
+ "# Regenerate: python codegen/generate_schema.py (CI/test: --check)",
146
+ f"# Source: {pin['repo']} @ {pin['tag']} ({pin['commit']})",
147
+ "",
148
+ '"""Schema provenance and the 22 element types, generated from the pinned schema distribution."""',
149
+ "",
150
+ f'SCHEMA_DIST_TAG = "{pin["tag"]}"',
151
+ f'SCHEMA_VERSION = "{pin["canonical_version"]}"',
152
+ f"SCHEMA_DIST_SERIAL = {pin['dist_serial']}",
153
+ f'SCHEMA_MANIFEST_SHA256 = "{pin["manifest_sha256"]}"',
154
+ "",
155
+ "# Canonical order, as the walk declares it.",
156
+ "ELEMENT_TYPES: tuple[str, ...] = (",
157
+ *[f' "{t}",' for t in types],
158
+ ")",
159
+ "",
160
+ "# type -> {wire field name: text | integer | single_link | multi_link}. Schema fields only:",
161
+ "# anything else on a body is server-managed, a tool's extension, or unknown.",
162
+ "FIELD_KINDS: dict[str, dict[str, str]] = {",
163
+ ]
164
+ for t in types:
165
+ lines.append(f' "{t}": {{')
166
+ lines.extend(f' "{name}": "{kind}",' for name, kind in kinds[t].items())
167
+ lines.append(" },")
168
+ lines += [
169
+ "}",
170
+ "",
171
+ "# type -> ((type column, id column), ...): each generic link's two halves. A write sets both or neither.",
172
+ "GENERIC_PAIRS: dict[str, tuple[tuple[str, str], ...]] = {",
173
+ *[f' "{t}": ({", ".join(f'("{a}", "{b}")' for a, b in pairs[t])},),' for t in types if t in pairs],
174
+ "}",
175
+ "",
176
+ ]
177
+ return "\n".join(lines)
178
+
179
+
180
+ def main() -> int:
181
+ ap = argparse.ArgumentParser()
182
+ ap.add_argument("--check", action="store_true")
183
+ args = ap.parse_args()
184
+ pin = verify_dist()
185
+ walk = load_walk()
186
+ types = element_types(walk)
187
+ text = render(pin, types, field_kinds(walk, types), generic_pairs(walk, types))
188
+ if args.check:
189
+ current = OUT.read_bytes().decode("utf-8") if OUT.is_file() else ""
190
+ if current != text:
191
+ print(f"STALE: {OUT} does not match what the pinned dist generates", file=sys.stderr)
192
+ return 1
193
+ print(f"ok: {OUT.name} matches {pin['tag']}")
194
+ return 0
195
+ OUT.write_bytes(text.encode("utf-8")) # bytes: LF on every platform
196
+ print(f"wrote {OUT} ({pin['tag']})")
197
+ return 0
198
+
199
+
200
+ if __name__ == "__main__":
201
+ raise SystemExit(main())
@@ -0,0 +1 @@
1
+ * -text
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 OnlyWorlds
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,73 @@
1
+ {
2
+ "files": {
3
+ ".gitattributes": "705fd4d6451a31d36b3df7de96f83f30ac976c9b4a6d1e51671d8e2f33e2d0da",
4
+ "example/hyperion/elements/ability/piano-playing--60195ff8.json": "a1ff537bb6db03944aa9aec97daef4d5847ed4bfa7150b71df9d533220f33cf6",
5
+ "example/hyperion/elements/character/mr-steinway--678bd862.json": "c0c93bff76d76f481a611b8772741d57a8adbb72d05e4465f9d30cf5ade3090c",
6
+ "example/hyperion/elements/character/rachmaninov--4c0d52b4.json": "15d2b8ee04ba6085b40bc06ef00e65e4fd392472ea47466e2e580b39248fcdee",
7
+ "example/hyperion/elements/character/the-consul--8b711e25.json": "f2a8c37169628c6109cdbe3bb3416a84ee9d8e1b3439e181cfc347b0be93ef04",
8
+ "example/hyperion/elements/collective/pack-of-bellowing-saurians--1218fc88.json": "02b87475b540d173ebc3418d8e825e207364cb3e6f87afebd738d6e35609f9f3",
9
+ "example/hyperion/elements/construct/hegemony-standard--e40d0001.json": "8db776fd7a3faa3a22626f2ca80e825853fa024ca2f4b18ae4070fbd82f5299f",
10
+ "example/hyperion/elements/construct/rachmaninov-s-prelude--6ebf1c1e.json": "588b1c509827371c4176c1cefd26f562e01e04664c134a3621607f193a38f1b5",
11
+ "example/hyperion/elements/construct/steinway-piano--d0a334b1.json": "2180305399f14be914b260d95d4859b0ae20a90e1767e103fc8015ae13ac10ad",
12
+ "example/hyperion/elements/creature/great-green-saurian--8406227d.json": "5dfc5230c54117b2974dee958a9c50623fd31a4c2a2823e255b1b12e75345d80",
13
+ "example/hyperion/elements/creature/greater-green-saurian--e79a4c7c.json": "af102041d4a69dc745ed29ce1e22db28da79af7c1e5a5927df92a72a2cb59947",
14
+ "example/hyperion/elements/event/the-primal-performance--8b461fb6.json": "df38b54bad28cf3ba84fed34f34912d389dfe502d34754d9818a8fe51b5fc1ba",
15
+ "example/hyperion/elements/event/the-raging-of-the-beasts--95bb5602.json": "d1b544e76495ab4bb4cd6bb19e9fa8d68ef7830187e107d4aafc82bb56d7b1d1",
16
+ "example/hyperion/elements/institution/the-hegemony--b6872c95.json": "69f99fa1e6f1f172eb941059c3b20f9c4e68dce2e6ed97c252c495b1c68e56cd",
17
+ "example/hyperion/elements/language/western-musical-notation--f856e44c.json": "d9ae2b48e93a8bf079d82520ea51d199baf401d666e107a4d25be198e0a89ae9",
18
+ "example/hyperion/elements/location/unnamed-planet--6283dd71.json": "f6249cc4920a66330ee90ee66ea5e592648b6812fc8dc0e893a6e00b1d2ea259",
19
+ "example/hyperion/elements/map/map-of-unnamed-planet--153cd291.json": "d65670dc9baeed1b4c67bcb0e0d7e4349f0d6b2b3bc1a9d25e1fd7a0d989a472",
20
+ "example/hyperion/elements/marker/swamps-point-0--f42a6e15.json": "925d954946e51a3cbf62e89ee917a71ec5454c9a67ed29fb1dfeeae432dfd3a8",
21
+ "example/hyperion/elements/marker/swamps-point-1--3f8fe28c.json": "0f8e4d0af04f75a3f6d1a2cd8f3fdd305e88aaa5ea72ccdbafee23488c374826",
22
+ "example/hyperion/elements/marker/swamps-point-2--811d2c15.json": "9157c840604764336ae40088949d2ec298fef482352bebfa2d31d784725ee2ce",
23
+ "example/hyperion/elements/marker/swamps-point-3--ea668c31.json": "4cc41e877b2a65ccd98f7ba99269f690a7376077b23d73b210ee3c88fafc8509",
24
+ "example/hyperion/elements/marker/swamps-point-4--4e843a51.json": "24b489a8a2a569aea6508e0c7b63c087c90fc03d9d7261bca0ef6593149a7efe",
25
+ "example/hyperion/elements/marker/swamps-point-5--48b3cfa2.json": "46106b8c97e2f95e4973384558e2e5d243fa1e8330c9d5751a3fb2c830e16ba7",
26
+ "example/hyperion/elements/narrative/chapter-1--39fd9536.json": "551f6a41ab05c7d8b45ca659c540bb8cdd0c2a492341d7e457190fcf2ebfd020",
27
+ "example/hyperion/elements/narrative/hyperion--bb17eaba.json": "a60867b9d2df1f9451498ac0d17ceb4fb7b911dd624ba02b8d5398d7c4e1feae",
28
+ "example/hyperion/elements/narrative/new-book--08afd034.json": "294a4efc6067c891dec3eaeec12e4f2137eca9ffc637210bda345dcf6e3bebdf",
29
+ "example/hyperion/elements/narrative/new-page--2f7bc2f5.json": "1fe86a75bf223a87b44f27b0555604abb9d63661a9ddceee2b0b2514092069b5",
30
+ "example/hyperion/elements/narrative/story-of-the-old-piano--2a6553ca.json": "dd0db85f8cebe8662c6d518a86cbd45b1301d251bf4538a302d30ad540314ef6",
31
+ "example/hyperion/elements/object/ebony-spaceship--f4e8e286.json": "ca58edba90b6a50016dac127ee2526247c1ab7b552774faddf1bd83e4ae263fe",
32
+ "example/hyperion/elements/object/spaceship-balcony--dd75923a.json": "4853b29c222c05d278ab947eb45b7d6dab23135b20db51c015629c167553dafe",
33
+ "example/hyperion/elements/object/the-consul-s-steinway--75bfd994.json": "514914783bfbd8e91d887cfc06068f020536523dee897aa7d94619c5c9973cdc",
34
+ "example/hyperion/elements/phenomenon/piano-music--5be90c7d.json": "020b2cc3e7c710ef43ec5ca948e40a911dc698b89da34d2da0dc7ff7c627d699",
35
+ "example/hyperion/elements/pin/the-consul-s-balcony-seat--df789513.json": "a6f91a3badcc97ef2a1e45cbda608934f773990e212bc4c83f9370b6f1a562c2",
36
+ "example/hyperion/elements/species/saurians--d24e2d1e.json": "90133114fb8c5f1642370741555f303f05580fbaa0cc63f50bba249d6f5720c6",
37
+ "example/hyperion/elements/title/consul-of-the-hegemony--241a68ab.json": "4723020b824c3abd22b3d2e20d23521ac68fbd3e2198527539fadfabf91cf1f3",
38
+ "example/hyperion/elements/trait/swamp-rage--dff0708a.json": "6bd7a73f73f9a8c37d8ca603af8209c75251ad524aa485fc06538a63c0123588",
39
+ "example/hyperion/elements/zone/swamps--d138f887.json": "d9e9caf3360daaecee51dab9716c7e4a444b47736284c0a6b4269d8b84ae2c45",
40
+ "example/hyperion/world.json": "eeaa6f45d6c232e7aae2114a40eb68a7d30fa7eadcb9e51e6a77908db8d738bb",
41
+ "LICENSE": "ab9469f20388ba53771315ed7cb94461d5e3de6e0113bd8f0280f3364434fe18",
42
+ "presentation.json": "776b5e48d9b27c1d99baabfdaffe7d2fe3995580450452c26e5ab7e30176264f",
43
+ "README.md": "0478b0be204b8683b3f8ea2da37d3ba3cc968a3ed6aa81c994c99242e5773892",
44
+ "schema/ability.yaml": "9fb53aa50af02134714e804b7e4c54652b63dd7adc11dd4aef991c64503205fc",
45
+ "schema/base_properties.yaml": "9d1d6b11ebae8405a3d9845fe8b186059c57e8f7474c9a0665d229025396cb2c",
46
+ "schema/character.yaml": "8bf572cb676024e86c4f7502a0b7f5bdfb2531071633f6f184e446f4e39efa3a",
47
+ "schema/collective.yaml": "da660c6bb2bfb4d2a2aeef958590a334dd86112477ae8112d9bf520b1a7d06f8",
48
+ "schema/construct.yaml": "0f2233208e03fa8cfd60ac1fd51cae2078713e8ca3f227437f4d3e520e7e3a6f",
49
+ "schema/creature.yaml": "66c8b7123614c4ef506386aae9a077d11babbb1ad365986ffcdb879100403c58",
50
+ "schema/event.yaml": "5843fd04bdfca18934c37c31c1f21777780bad7381bf4b3a8e731f21e4d99558",
51
+ "schema/family.yaml": "2f3b9225d46c15138b714d4ac5157b1ea2f837a2046431454c22ded226a1f5ba",
52
+ "schema/institution.yaml": "8f1912ce0588315eb982b7b32b72dbd2d641dce235533e33527b51767e7075e4",
53
+ "schema/language.yaml": "272fdc87bfaf3b91ca19df7354ba0a1986d9933612a740e0c8cfcc9e86033f78",
54
+ "schema/law.yaml": "51a35f12128ed6cb0069086aebc2523694373a50fff659657109210264ef3dd8",
55
+ "schema/location.yaml": "6833171e55989468f11105cfad7ac2b1d7812b112392765dc2572fd03a80ab57",
56
+ "schema/map.yaml": "5a3ee73f68f4c096c9969b4bcec44e0c4d6be4e826d0496f1cf7e81736d10375",
57
+ "schema/marker.yaml": "2843eff80fc4360e0d8e9fca5ade1c9337d88c99089a79182e3454e8056d3ed0",
58
+ "schema/narrative.yaml": "d5ab13a6278e8fc72ffca1ed2429bc2dc73e6ea999396c3825a0ac68b80f4e21",
59
+ "schema/object.yaml": "697a853295302aa036fbf26190b8b754a5d177675d97421cab52026ffe1bb2f9",
60
+ "schema/phenomenon.yaml": "27dc2d0557fb4d5e9b08769b989dfb19c61ead77670c34e152ef1462918fdd16",
61
+ "schema/pin.yaml": "2554639d3bc0ef3be6477253c3486521b01e26f7eb6ec38c870120e9257486b1",
62
+ "schema/relation.yaml": "40be0989442bddeb84d11762250c92e6026065d174ae32d3a00c2c650d857823",
63
+ "schema/species.yaml": "6df722fe713b1b5adbde591b4664e808292126b489d99c7b469349b5af7e6cd5",
64
+ "schema/title.yaml": "36ef98440f238c731bdb433102fda890794cde31031e05ce8fe56b70801e96ad",
65
+ "schema/trait.yaml": "261df723f603809d71c9ddf5faf27a0f0ba92186f544e56cea95911051d2359d",
66
+ "schema/world.yaml": "f4e875e80802a77f21afe8334cdb5918299d64cb52007abd050b131d59cf9d04",
67
+ "schema/zone.yaml": "9474902bf71703ac82495b28ad253266edc0ffc69894ac8846e3934b268c81a5",
68
+ "schema.json": "9ebf22d585e30d72dd4d17a1b09016d620a5ef0b2b806c5d0800efb0588bbb2e",
69
+ "VERSION": "5a20f5f044a10a717a369d6424dacf43d299b8cebfa3e1e23cad12cfda42aea8",
70
+ "walk/rulings.yaml": "75fff534c7dd47763ba79d489a31c7792b4f0569e5d3cbbf44f46e677e92deef",
71
+ "walk/schema_walk.py": "519f431f496e28b2745d6c3773ecbd0174b26175b10fcf49eb527aaed43b895b"
72
+ }
73
+ }