onlyworlds 0.1.0__py3-none-any.whl

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.
onlyworlds/__init__.py ADDED
@@ -0,0 +1,79 @@
1
+ """OnlyWorlds for Python: the world folder (read, write, push) and a client for the v2 API.
2
+
3
+ Folder format: v0.3.7 (``FORMAT_VERSION``). Schema: generated from the pinned
4
+ schema distribution (``SCHEMA_DIST_TAG``, ``SCHEMA_VERSION``); the package
5
+ version runs on its own semver.
6
+ """
7
+
8
+ from ._ids import uuid7
9
+ from ._schema import ELEMENT_TYPES, FIELD_KINDS, SCHEMA_DIST_TAG, SCHEMA_MANIFEST_SHA256, SCHEMA_VERSION
10
+ from ._version import __version__
11
+ from .errors import parse_retry_after
12
+ from .export import export_world
13
+ from .folder import (
14
+ FORMAT_VERSION,
15
+ Folder,
16
+ FolderElement,
17
+ SkippedFile,
18
+ WriteReport,
19
+ element_filename,
20
+ read_folder,
21
+ resolve_filenames,
22
+ slugify,
23
+ write_folder,
24
+ )
25
+ from .http import (
26
+ API_BASE,
27
+ READ_ONLY_FIELDS,
28
+ ApiError,
29
+ BulkResult,
30
+ ChangeWalk,
31
+ Client,
32
+ Response,
33
+ RetryPolicy,
34
+ Transport,
35
+ UrllibTransport,
36
+ check_kinds,
37
+ sanitize_payload,
38
+ )
39
+ from .push import Patch, PushPlan, PushResult, plan_push, push, verify_level
40
+
41
+ __all__ = [
42
+ "API_BASE",
43
+ "ELEMENT_TYPES",
44
+ "FIELD_KINDS",
45
+ "FORMAT_VERSION",
46
+ "READ_ONLY_FIELDS",
47
+ "SCHEMA_DIST_TAG",
48
+ "SCHEMA_MANIFEST_SHA256",
49
+ "SCHEMA_VERSION",
50
+ "ApiError",
51
+ "BulkResult",
52
+ "ChangeWalk",
53
+ "Client",
54
+ "Folder",
55
+ "FolderElement",
56
+ "Patch",
57
+ "PushPlan",
58
+ "PushResult",
59
+ "Response",
60
+ "RetryPolicy",
61
+ "SkippedFile",
62
+ "Transport",
63
+ "UrllibTransport",
64
+ "WriteReport",
65
+ "__version__",
66
+ "check_kinds",
67
+ "element_filename",
68
+ "export_world",
69
+ "parse_retry_after",
70
+ "plan_push",
71
+ "push",
72
+ "read_folder",
73
+ "resolve_filenames",
74
+ "sanitize_payload",
75
+ "slugify",
76
+ "uuid7",
77
+ "verify_level",
78
+ "write_folder",
79
+ ]
onlyworlds/_ids.py ADDED
@@ -0,0 +1,28 @@
1
+ """UUIDv7 minting (RFC 9562), because Python's ``uuid`` only gains ``uuid7`` in 3.14.
2
+
3
+ keel mints v7 ids itself and the standard says elements carry them, so a client that
4
+ creates an element without an id gives it one that sorts by creation time. v7 is a
5
+ default, never a requirement: a caller's own v4 id stays valid forever. Never order
6
+ elements by id (worlds also hold v4 and legacy ids); use ``created_at``.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ import time
13
+ import uuid
14
+ from collections.abc import Callable
15
+
16
+ __all__ = ["uuid7"]
17
+
18
+
19
+ def uuid7(now_ms: int | None = None, rand: Callable[[int], bytes] = os.urandom) -> str:
20
+ """48-bit Unix milliseconds, version 7, variant 10, 74 random bits."""
21
+ ms = int(time.time() * 1000) if now_ms is None else now_ms
22
+ if not 0 <= ms < 2**48:
23
+ raise ValueError(f"timestamp {ms} ms does not fit 48 bits")
24
+ b = bytearray(rand(16))
25
+ b[0:6] = ms.to_bytes(6, "big")
26
+ b[6] = (b[6] & 0x0F) | 0x70
27
+ b[8] = (b[8] & 0x3F) | 0x80
28
+ return str(uuid.UUID(bytes=bytes(b)))
onlyworlds/_jsonfmt.py ADDED
@@ -0,0 +1,135 @@
1
+ """On-disk JSON bytes for the folder format (spec §5, ruled v0.3.3).
2
+
3
+ Normative: UTF-8 without BOM, LF, 2-space indent, one trailing newline, key order
4
+ as received, *matching the reference implementation's*
5
+ ``JSON.stringify(value, null, 2)``. Two writers that agree on layout but not on
6
+ bytes make every diff noise, which defeats the format's purpose.
7
+
8
+ ``json.dumps(obj, indent=2, ensure_ascii=False)`` gets most of the way there and
9
+ differs from ``JSON.stringify`` in exactly these places, each handled here:
10
+
11
+ - **Floats with an integral value**: Python writes ``1.0``, JavaScript ``1``.
12
+ - **Exponent ranges**: Python writes ``1e+16`` and ``1e-05``; JavaScript writes
13
+ ``10000000000000000`` and ``0.00001`` (ECMA-262 Number::toString switches to
14
+ exponent form only outside 1e-7 <= |x| < 1e21, and writes ``1e-7``, not ``1e-07``).
15
+ - **Negative zero**: Python ``-0.0``, JavaScript ``0``.
16
+ - **Lone surrogates** (legal in a Python str after ``json.loads("\\ud800")``):
17
+ Python writes the raw code point and the UTF-8 encode then fails; JavaScript
18
+ (ES2019 well-formed stringify) writes ``\\ud800``.
19
+
20
+ Deliberately NOT emulated (each reported, not hidden):
21
+
22
+ - **Non-finite numbers**: ``JSON.stringify`` silently writes ``null``. That is a
23
+ value change, so this raises instead.
24
+ - **Integers beyond 2**53**: JavaScript has already lost precision at parse time;
25
+ Python keeps the exact value and writes it. Python is the faithful one.
26
+ - **Array-index key order**: a JavaScript object enumerates integer-like keys
27
+ ("0", "12") first, ascending, whatever order they arrived in. §5 says key
28
+ order *as received*, so this writer keeps the received order and differs from
29
+ JavaScript only on objects that have such keys.
30
+
31
+ The digits themselves agree: Python's ``repr(float)`` and ECMAScript both emit the
32
+ shortest string that round-trips, so only the formatting around them is ported.
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ import math
38
+ import re
39
+ from collections.abc import Mapping, Sequence
40
+ from typing import Any
41
+
42
+ _REPR = re.compile(r"(-?)(\d+)(?:\.(\d+))?(?:e([+-]\d+))?")
43
+ _ESCAPE = re.compile(r'[\x00-\x1f"\\\ud800-\udfff]')
44
+ _SHORT = {'"': '\\"', "\\": "\\\\", "\b": "\\b", "\f": "\\f", "\n": "\\n", "\r": "\\r", "\t": "\\t"}
45
+
46
+
47
+ def js_number(x: float) -> str:
48
+ """Format a float exactly as ECMAScript Number::toString (and so JSON.stringify) does."""
49
+ if not math.isfinite(x):
50
+ raise ValueError(f"non-finite number {x!r} has no JSON form (JSON.stringify would silently write null)")
51
+ if x == 0:
52
+ return "0" # covers -0.0
53
+ m = _REPR.fullmatch(repr(x))
54
+ if m is None: # pragma: no cover - repr(float) always matches
55
+ raise ValueError(f"unexpected float repr {x!r}")
56
+ sign, intpart, frac, exp = m.group(1), m.group(2), m.group(3) or "", m.group(4)
57
+ raw = intpart + frac
58
+ stripped = raw.lstrip("0")
59
+ # value = 0.<digits> x 10**n
60
+ n = len(intpart) + int(exp or 0) - (len(raw) - len(stripped))
61
+ digits = stripped.rstrip("0")
62
+ k = len(digits)
63
+ if k <= n <= 21:
64
+ body = digits + "0" * (n - k)
65
+ elif 0 < n <= 21:
66
+ body = digits[:n] + "." + digits[n:]
67
+ elif -6 < n <= 0:
68
+ body = "0." + "0" * (-n) + digits
69
+ else:
70
+ e = n - 1
71
+ body = digits[0] + ("." + digits[1:] if k > 1 else "") + "e" + ("+" if e >= 0 else "-") + str(abs(e))
72
+ return sign + body
73
+
74
+
75
+ def _string(s: str) -> str:
76
+ def esc(m: re.Match[str]) -> str:
77
+ c = m.group(0)
78
+ return _SHORT.get(c) or f"\\u{ord(c):04x}"
79
+
80
+ return '"' + _ESCAPE.sub(esc, s) + '"'
81
+
82
+
83
+ def _dump(v: Any, indent: str, out: list[str]) -> None:
84
+ if v is None:
85
+ out.append("null")
86
+ elif v is True:
87
+ out.append("true")
88
+ elif v is False:
89
+ out.append("false")
90
+ elif isinstance(v, int):
91
+ out.append(str(v))
92
+ elif isinstance(v, float):
93
+ out.append(js_number(v))
94
+ elif isinstance(v, str):
95
+ out.append(_string(v))
96
+ elif isinstance(v, Mapping):
97
+ if not v:
98
+ out.append("{}")
99
+ return
100
+ inner = indent + " "
101
+ out.append("{")
102
+ first = True
103
+ for k, item in v.items():
104
+ if not isinstance(k, str):
105
+ raise TypeError(f"JSON object keys must be str, got {type(k).__name__}")
106
+ out.append("\n" + inner if first else ",\n" + inner)
107
+ first = False
108
+ out.append(_string(k) + ": ")
109
+ _dump(item, inner, out)
110
+ out.append("\n" + indent + "}")
111
+ elif isinstance(v, Sequence) and not isinstance(v, (bytes, bytearray)):
112
+ if not v:
113
+ out.append("[]")
114
+ return
115
+ inner = indent + " "
116
+ out.append("[")
117
+ for i, item in enumerate(v):
118
+ out.append(("\n" if i == 0 else ",\n") + inner)
119
+ _dump(item, inner, out)
120
+ out.append("\n" + indent + "]")
121
+ else:
122
+ raise TypeError(f"not JSON-serializable: {type(v).__name__}")
123
+
124
+
125
+ def serialize(value: Any) -> str:
126
+ """The canonical on-disk text: ``JSON.stringify(value, null, 2) + "\\n"``."""
127
+ out: list[str] = []
128
+ _dump(value, "", out)
129
+ out.append("\n")
130
+ return "".join(out)
131
+
132
+
133
+ def encode(value: Any) -> bytes:
134
+ """The canonical on-disk bytes: UTF-8, no BOM, LF."""
135
+ return serialize(value).encode("utf-8")