zipzi 0.1.0a2__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 (81) hide show
  1. zipzi-0.1.0a2/LICENSE-MIT +21 -0
  2. zipzi-0.1.0a2/PKG-INFO +40 -0
  3. zipzi-0.1.0a2/README.md +30 -0
  4. zipzi-0.1.0a2/THIRD_PARTY_NOTICES.md +33 -0
  5. zipzi-0.1.0a2/pyproject.toml +16 -0
  6. zipzi-0.1.0a2/python/zipzi/__init__.py +69 -0
  7. zipzi-0.1.0a2/python/zipzi/_backend_types.py +3 -0
  8. zipzi-0.1.0a2/python/zipzi/_calls.py +119 -0
  9. zipzi-0.1.0a2/python/zipzi/_documentation.py +49 -0
  10. zipzi-0.1.0a2/python/zipzi/_dto_support.py +61 -0
  11. zipzi-0.1.0a2/python/zipzi/_endpoint.py +148 -0
  12. zipzi-0.1.0a2/python/zipzi/_errors.py +131 -0
  13. zipzi-0.1.0a2/python/zipzi/_extensions.py +38 -0
  14. zipzi-0.1.0a2/python/zipzi/_facade_support.py +62 -0
  15. zipzi-0.1.0a2/python/zipzi/_generated/__init__.py +2 -0
  16. zipzi-0.1.0a2/python/zipzi/_generated/backend_bindings.py +330 -0
  17. zipzi-0.1.0a2/python/zipzi/_generated/binary.py +4897 -0
  18. zipzi-0.1.0a2/python/zipzi/_generated/binding_values.py +274 -0
  19. zipzi-0.1.0a2/python/zipzi/_generated/catalog.py +36651 -0
  20. zipzi-0.1.0a2/python/zipzi/_generated/discovery.py +92 -0
  21. zipzi-0.1.0a2/python/zipzi/_generated/event_values.py +199 -0
  22. zipzi-0.1.0a2/python/zipzi/_generated/facades.py +140 -0
  23. zipzi-0.1.0a2/python/zipzi/_generated/mesh_ops.py +148 -0
  24. zipzi-0.1.0a2/python/zipzi/_generated/meshes.py +145 -0
  25. zipzi-0.1.0a2/python/zipzi/_generated/modeling.py +17008 -0
  26. zipzi-0.1.0a2/python/zipzi/_generated/parameter_values.py +214 -0
  27. zipzi-0.1.0a2/python/zipzi/_generated/plan_intents.py +54 -0
  28. zipzi-0.1.0a2/python/zipzi/_generated/plan_values.py +40 -0
  29. zipzi-0.1.0a2/python/zipzi/_generated/planning.py +10 -0
  30. zipzi-0.1.0a2/python/zipzi/_generated/protocols.py +8 -0
  31. zipzi-0.1.0a2/python/zipzi/_generated/reference_values.py +65 -0
  32. zipzi-0.1.0a2/python/zipzi/_generated/request_directory.py +103 -0
  33. zipzi-0.1.0a2/python/zipzi/_generated/resource_backends.py +13 -0
  34. zipzi-0.1.0a2/python/zipzi/_generated/resource_ops.py +82 -0
  35. zipzi-0.1.0a2/python/zipzi/_generated/resource_values.py +54 -0
  36. zipzi-0.1.0a2/python/zipzi/_generated/sampling_values.py +304 -0
  37. zipzi-0.1.0a2/python/zipzi/_generated/scene.py +915 -0
  38. zipzi-0.1.0a2/python/zipzi/_jobs.py +151 -0
  39. zipzi-0.1.0a2/python/zipzi/_lifecycle.py +523 -0
  40. zipzi-0.1.0a2/python/zipzi/_observation.py +26 -0
  41. zipzi-0.1.0a2/python/zipzi/_operations.py +44 -0
  42. zipzi-0.1.0a2/python/zipzi/_owned.py +64 -0
  43. zipzi-0.1.0a2/python/zipzi/_planning.py +30 -0
  44. zipzi-0.1.0a2/python/zipzi/_plans.py +49 -0
  45. zipzi-0.1.0a2/python/zipzi/_presentation.py +46 -0
  46. zipzi-0.1.0a2/python/zipzi/_preview.py +115 -0
  47. zipzi-0.1.0a2/python/zipzi/_queries.py +45 -0
  48. zipzi-0.1.0a2/python/zipzi/_recovery.py +43 -0
  49. zipzi-0.1.0a2/python/zipzi/_request_methods.py +2 -0
  50. zipzi-0.1.0a2/python/zipzi/_results.py +45 -0
  51. zipzi-0.1.0a2/python/zipzi/_spatial.py +32 -0
  52. zipzi-0.1.0a2/python/zipzi/_transport.py +19 -0
  53. zipzi-0.1.0a2/python/zipzi/_values.py +195 -0
  54. zipzi-0.1.0a2/python/zipzi/_windows_endpoint.py +95 -0
  55. zipzi-0.1.0a2/python/zipzi/_windows_pipe.py +171 -0
  56. zipzi-0.1.0a2/python/zipzi/animation.py +5 -0
  57. zipzi-0.1.0a2/python/zipzi/backends/__init__.py +82 -0
  58. zipzi-0.1.0a2/python/zipzi/backends/_native_types.py +80 -0
  59. zipzi-0.1.0a2/python/zipzi/backends/_transfers.py +192 -0
  60. zipzi-0.1.0a2/python/zipzi/backends/connected.py +301 -0
  61. zipzi-0.1.0a2/python/zipzi/backends/local.py +71 -0
  62. zipzi-0.1.0a2/python/zipzi/bindings.py +5 -0
  63. zipzi-0.1.0a2/python/zipzi/buffers.py +98 -0
  64. zipzi-0.1.0a2/python/zipzi/docs/editor_handoff.py +52 -0
  65. zipzi-0.1.0a2/python/zipzi/docs/guide.md +290 -0
  66. zipzi-0.1.0a2/python/zipzi/docs/recipes.py +138 -0
  67. zipzi-0.1.0a2/python/zipzi/docs/reference.md +26968 -0
  68. zipzi-0.1.0a2/python/zipzi/events.py +103 -0
  69. zipzi-0.1.0a2/python/zipzi/inspection.py +60 -0
  70. zipzi-0.1.0a2/python/zipzi/mesh_generation.py +83 -0
  71. zipzi-0.1.0a2/python/zipzi/meshes.py +6 -0
  72. zipzi-0.1.0a2/python/zipzi/modeling.py +19 -0
  73. zipzi-0.1.0a2/python/zipzi/observation.py +4 -0
  74. zipzi-0.1.0a2/python/zipzi/ops.py +13 -0
  75. zipzi-0.1.0a2/python/zipzi/parameters.py +5 -0
  76. zipzi-0.1.0a2/python/zipzi/py.typed +0 -0
  77. zipzi-0.1.0a2/python/zipzi/references.py +138 -0
  78. zipzi-0.1.0a2/python/zipzi/resources.py +133 -0
  79. zipzi-0.1.0a2/python/zipzi/sampling.py +20 -0
  80. zipzi-0.1.0a2/python/zipzi/scene.py +4 -0
  81. zipzi-0.1.0a2/python/zipzi/spatial.py +4 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zipzi 2D Contributors
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.
zipzi-0.1.0a2/PKG-INFO ADDED
@@ -0,0 +1,40 @@
1
+ Metadata-Version: 2.4
2
+ Name: zipzi
3
+ Version: 0.1.0a2
4
+ Summary: Typed Python client for the Zipzi authoring Runtime
5
+ License-Expression: MIT
6
+ License-File: LICENSE-MIT
7
+ License-File: THIRD_PARTY_NOTICES.md
8
+ Requires-Python: >=3.14, <3.15
9
+ Description-Content-Type: text/markdown
10
+
11
+ # zipzi
12
+
13
+ Pure Python authoring client for Zipzi. Import it as `zipzi` and use
14
+ `zipzi.connect()` to attach to a resident desktop host. Install `zipzi-native`
15
+ to use the in-process `zipzi.local()` backend.
16
+
17
+ Use `runtime.sessions.list()` and `sessions.attach(info.id)` to work with an
18
+ existing document. Changes made through an Edit share the Editor's document and
19
+ undo history; saving is separate. Reads return source-bound `Snapshot` or `Batch`
20
+ values. Jobs must be waited or closed to release their resources.
21
+
22
+ If a response is lost, recover the original call with `OutcomeUnknown.recover()`
23
+ or `zipzi.recover_call(bound_sdk_method, ...)`. A timed-out `job.wait()` does
24
+ not cancel the job. Close an unresolved Runtime only after recovery or with
25
+ `abandon_pending=True`.
26
+
27
+ The package includes `zipzi/docs/guide.md`, `reference.md` and `recipes.py`;
28
+ `zipzi.describe("zipzi")` works offline. See the [package source](https://github.com/getzipzi/zipzi-2d/tree/main/crates/zipzi-python2)
29
+ for setup, examples and tests.
30
+
31
+ For an Editor handoff, run:
32
+
33
+ ```sh
34
+ python -I -m zipzi.docs.editor_handoff list
35
+ python -I -m zipzi.docs.editor_handoff rename \
36
+ --runtime RUNTIME_UUID --session SESSION_SERIAL --mesh MESH_UUID --name "Refined face"
37
+ ```
38
+
39
+ Pass `--endpoint /absolute/path/to/profile.sock` before the subcommand to select
40
+ a particular host.
@@ -0,0 +1,30 @@
1
+ # zipzi
2
+
3
+ Pure Python authoring client for Zipzi. Import it as `zipzi` and use
4
+ `zipzi.connect()` to attach to a resident desktop host. Install `zipzi-native`
5
+ to use the in-process `zipzi.local()` backend.
6
+
7
+ Use `runtime.sessions.list()` and `sessions.attach(info.id)` to work with an
8
+ existing document. Changes made through an Edit share the Editor's document and
9
+ undo history; saving is separate. Reads return source-bound `Snapshot` or `Batch`
10
+ values. Jobs must be waited or closed to release their resources.
11
+
12
+ If a response is lost, recover the original call with `OutcomeUnknown.recover()`
13
+ or `zipzi.recover_call(bound_sdk_method, ...)`. A timed-out `job.wait()` does
14
+ not cancel the job. Close an unresolved Runtime only after recovery or with
15
+ `abandon_pending=True`.
16
+
17
+ The package includes `zipzi/docs/guide.md`, `reference.md` and `recipes.py`;
18
+ `zipzi.describe("zipzi")` works offline. See the [package source](https://github.com/getzipzi/zipzi-2d/tree/main/crates/zipzi-python2)
19
+ for setup, examples and tests.
20
+
21
+ For an Editor handoff, run:
22
+
23
+ ```sh
24
+ python -I -m zipzi.docs.editor_handoff list
25
+ python -I -m zipzi.docs.editor_handoff rename \
26
+ --runtime RUNTIME_UUID --session SESSION_SERIAL --mesh MESH_UUID --name "Refined face"
27
+ ```
28
+
29
+ Pass `--endpoint /absolute/path/to/profile.sock` before the subcommand to select
30
+ a particular host.
@@ -0,0 +1,33 @@
1
+ # Third-party notices
2
+
3
+ The repository-level MIT License applies only where a file, directory,
4
+ dependency, or submodule does not carry a different notice. The following
5
+ upstream works retain their original copyrights and license terms.
6
+
7
+
8
+ ## PurismCore
9
+
10
+ `third_party/purism-core` is a forked Git submodule of
11
+ [PurismCore](https://github.com/SakuraMotion/PurismCore).
12
+
13
+ Copyright (c) 2026 Sakura Motion Project.
14
+
15
+ PurismCore is distributed under the MIT License included in that submodule.
16
+
17
+ ## Imported Editor and patched dependencies
18
+
19
+ Editor-owned source and assets retain GPL-3.0-only; see [LICENSING](LICENSING.md)
20
+ and the full [GPL text](LICENSE-GPL-3.0). Patched GPUI sources now live in `vendor/`;
21
+ their original notices and [dependency/patch record](vendor/README.md#dependencies)
22
+ remain authoritative. Directory flattening does not relicense third-party code.
23
+
24
+ ## PSD2Live adaptive mesh generator
25
+
26
+ `crates/zipzi-editor-core/src/adaptive_mesh/` is a Rust adaptation of the automatic
27
+ mesh-generation code in [PSD2Live](https://github.com/tsunehimatoi/psd2live),
28
+ revision `692986728e2b7b9254a41a22bdce1656bc0c12f9`, by the PSD2Live contributors.
29
+ It retains **GPL-3.0-only**, including its derived synthetic reference outputs in
30
+ `fixtures/mesh-generation/psd2live.json`. The original and adapted file mapping and
31
+ numerical substitutions are documented in that module's README. These sources
32
+ are part of the GPL Editor, not the MIT SDK. The full license is included in
33
+ `LICENSE-GPL-3.0`.
@@ -0,0 +1,16 @@
1
+ [build-system]
2
+ requires = ["uv_build==0.10.7"]
3
+ build-backend = "uv_build"
4
+
5
+ [project]
6
+ name = "zipzi"
7
+ version = "0.1.0a2"
8
+ requires-python = ">=3.14,<3.15"
9
+ description = "Typed Python client for the Zipzi authoring Runtime"
10
+ license = "MIT"
11
+ license-files = ["LICENSE-MIT", "THIRD_PARTY_NOTICES.md"]
12
+ readme = "README.md"
13
+
14
+ [tool.uv.build-backend]
15
+ module-name = "zipzi"
16
+ module-root = "python"
@@ -0,0 +1,69 @@
1
+ """Typed Python authoring API backed by the Zipzi Runtime."""
2
+ from pathlib import Path
3
+ from . import modeling as modeling, bindings, sampling, events, meshes, ops, parameters, resources, scene, references, animation, spatial, observation, inspection
4
+ from ._plans import Plan
5
+ from ._generated.plan_values import PlanCost, PlanReport
6
+ from .resources import PreparedImage
7
+ from .buffers import Buffer
8
+ from ._errors import Busy, JobTimeout, OutcomeUnknown, PendingRequest, RecoveryAction, SdkFailure
9
+ from ._lifecycle import Edit, EditState, Job, JobState, Runtime, Session
10
+ from ._calls import recover_call as recover_call
11
+ from ._results import Batch, Page, PageCursor, Snapshot
12
+ from ._values import ConnectionInfo, ConnectionLimits, Receipt, SaveReceipt, SessionId, SessionInfo, Source, Version, WriteResult
13
+
14
+ def local() -> Runtime:
15
+ 'Explicitly create an owned in-process Runtime with the paired optional native wheel. Importing zipzi alone does not start it. Use as a context manager; close releases its sessions without saving or connecting to another host.'
16
+ from .backends.local import LocalBackend
17
+ return Runtime(LocalBackend())
18
+
19
+ def connect(*, endpoint: Path | None = None, start: bool = False, host: Path | None = None,
20
+ timeout: float = 10., startup_timeout: float = 15., sdk_build: str | None = None) -> Runtime:
21
+ """Connect to an explicit/discovered host and return an owned Runtime client.
22
+
23
+ endpoint is an absolute private Unix socket path or a local Windows Named
24
+ Pipe name; None discovers it from ZIPZI_RUNTIME_DIR/ZIPZI_RUNTIME_PROFILE.
25
+ On Windows, start=True requires the discovered endpoint (or None).
26
+ start=False never launches a
27
+ process. With start=True, host is an absolute executable, or discovery uses
28
+ ZIPZI_EDITOR_HOST and the supported app locations. host requires start=True.
29
+ timeout is the positive finite per-request transport timeout in seconds;
30
+ startup_timeout is the positive finite total startup/handshake budget.
31
+ sdk_build=None accepts the compatible host build; an explicit string also
32
+ requires an exact build match. Protocol and required capabilities always
33
+ match strictly; installing native never changes the selected execution host.
34
+
35
+ This blocks for connection/startup, raising ValueError for invalid options,
36
+ PermissionError for unsafe endpoint ownership, OS connection errors when
37
+ no host is listening, or SdkFailure for pairing/startup refusal. Runtime
38
+ resources are bounded by host limits. Close the returned client or use with;
39
+ this releases its leases while formal Connected sessions remain resident.
40
+ A request that later becomes unknown must recover on this original Runtime;
41
+ calling connect again does not replay or resolve that request.
42
+
43
+ Example: with zipzi.connect(endpoint=absolute_socket_path) as runtime: ...
44
+ """
45
+ from ._endpoint import connect_backend
46
+ return Runtime(connect_backend(endpoint, start=start, host=host, timeout=timeout,
47
+ startup_timeout=startup_timeout, sdk_build=sdk_build))
48
+
49
+ __all__ = ["local", "connect", "Runtime", "Session", "Edit", "Job", "SdkFailure", "Busy", "OutcomeUnknown",
50
+ "PendingRequest", "Version", "Source", "Receipt", "SaveReceipt", "SessionId", "SessionInfo", "ConnectionInfo", "ConnectionLimits", "WriteResult", "modeling", "bindings", "sampling", "events", "meshes", "ops", "parameters", "resources", "scene", "references", "Plan", "PreparedImage", "Buffer"]
51
+ __all__ += ["animation", "spatial", "observation", "inspection"]
52
+
53
+ __all__ += ["recover_call", "EditState", "JobState", "JobTimeout", "RecoveryAction"]
54
+
55
+ from ._request_methods import register as _register_requests
56
+ _register_requests()
57
+
58
+ __all__ += ["Snapshot", "Batch", "Page", "PageCursor"]
59
+
60
+ __all__ += ["PlanCost", "PlanReport"]
61
+
62
+ from ._generated.discovery import RuntimeCapabilities, RuntimeLimits, ObjectSummary
63
+ __all__ += ["RuntimeCapabilities", "RuntimeLimits", "ObjectSummary"]
64
+
65
+ from ._documentation import ApiDescription, describe
66
+ __all__ += ["ApiDescription", "describe"]
67
+
68
+ from . import mesh_generation as mesh_generation
69
+ __all__ += ["mesh_generation"]
@@ -0,0 +1,3 @@
1
+ """Finite internal task discriminators; no domain imports."""
2
+ from typing import Literal
3
+ JobKind = Literal["published", "saved", "opened", "sampled", "planned", "project", "image", "preview", "spatial", "observation"]
@@ -0,0 +1,119 @@
1
+ """One complete public return contract across request and transfer recovery."""
2
+ from __future__ import annotations
3
+
4
+ from collections.abc import Callable
5
+ from types import MethodType
6
+ from typing import Generic, Never, ParamSpec, TypeVar, cast
7
+ from ._errors import OutcomeUnknown, PendingRequest, SdkFailure
8
+
9
+ T = TypeVar("T", covariant=True)
10
+ U = TypeVar("U")
11
+ P = ParamSpec("P")
12
+
13
+
14
+ def _resolved_call(*_: object) -> Never:
15
+ raise SdkFailure("NO_PENDING_REQUEST", "This public call has already been resolved")
16
+
17
+
18
+ class Call(Generic[T]):
19
+ """Retain owned inputs, raw continuation and the complete return projection.
20
+
21
+ The backend owns the immutable wire bytes and transfer stage. A Call never
22
+ invokes its original execute closure during recovery. The same decoder is
23
+ applied once after either normal execution or raw continuation succeeds.
24
+ """
25
+ def __init__(self, operation: str, execute: Callable[[], object], decode: Callable[[object], T], *,
26
+ resume: Callable[[], object] | None = None, inputs: tuple[object, ...] = (),
27
+ on_unknown: Callable[[OutcomeUnknown], None] | None = None,
28
+ on_error: Callable[[BaseException], None] | None = None):
29
+ self.operation, self.inputs = operation, inputs
30
+ self._execute, self._decode, self._resume = execute, decode, resume
31
+ self._explicit_resume = resume is not None
32
+ self._on_unknown, self._on_error = on_unknown, on_error
33
+ self.phase = "prepared"
34
+ self.pending_request: PendingRequest | None = None
35
+ self._bound_resume: Callable[[], object] | None = None
36
+
37
+ def run(self) -> T:
38
+ if self.phase != "prepared":
39
+ raise SdkFailure("NO_PENDING_REQUEST", "This public call has already started")
40
+ return self._invoke(self._execute)
41
+
42
+ def recover(self, expected: PendingRequest) -> T:
43
+ if self.phase != "unknown" or self.pending_request != expected or self._resume is None:
44
+ raise SdkFailure("NO_PENDING_REQUEST", "This public call has already been resolved")
45
+ return self._invoke(self._resume)
46
+
47
+ def _finish(self, phase: str) -> None:
48
+ self.phase, self.pending_request = phase, None
49
+ self._execute, self._resume, self._decode = _resolved_call, None, _resolved_call
50
+ self._bound_resume = self._on_unknown = self._on_error = None
51
+ self.inputs = ()
52
+
53
+ def _invoke(self, action: Callable[[], object]) -> T:
54
+ self.phase = "request"
55
+ try:
56
+ value = self._decode(action())
57
+ except OutcomeUnknown as error:
58
+ # An outer public projection captures the inner Call continuation
59
+ # before replacing the exception callback with its own full result.
60
+ if not self._explicit_resume and error._resume is not self._bound_resume and error._resume is not None:
61
+ self._resume = error._resume
62
+ self.phase, self.pending_request = "unknown", error.pending
63
+ expected = error.pending
64
+ self._bound_resume = lambda: self.recover(expected)
65
+ error._resume = self._bound_resume
66
+ if self._on_unknown is not None:
67
+ self._on_unknown(error)
68
+ raise
69
+ except BaseException as error:
70
+ callback = self._on_error
71
+ self._finish("failed")
72
+ if callback is not None:
73
+ callback(error)
74
+ raise
75
+ self._finish("resolved")
76
+ return value
77
+
78
+
79
+ def known(execute: Callable[[], U], resume: Callable[[], U]) -> U:
80
+ """Adapter for existing complete decoders while facades move to generation."""
81
+ operation = execute.__qualname__.split(".<locals>", 1)[0]
82
+ return Call(operation, execute, lambda value: cast(U, value), resume=resume).run()
83
+
84
+
85
+ def project(execute: Callable[[], U], projection: Callable[[U], T], *, operation: str,
86
+ inputs: tuple[object, ...] = ()) -> T:
87
+ return Call(operation, execute, lambda value: projection(cast(U, value)), inputs=inputs).run()
88
+
89
+
90
+ def single_id(result: object) -> str:
91
+ from ._values import WriteResult
92
+ if not isinstance(result, WriteResult) or len(result.ids) != 1:
93
+ raise SdkFailure("INVALID_RESULT", "A create operation must return exactly one ID")
94
+ return result.ids[0]
95
+
96
+
97
+ _REQUEST_METHODS: set[object] = set()
98
+
99
+
100
+ def register_request(method: object) -> None:
101
+ """Only library-owned public method implementations enter this directory."""
102
+ _REQUEST_METHODS.add(getattr(method, "__func__", method))
103
+
104
+
105
+ def recover_call(method: Callable[P, U], /, *args: P.args, **kwargs: P.kwargs) -> U:
106
+ """Call a registered SDK request method once and recover its Call once.
107
+
108
+ Parameters exactly match the bound SDK method, preserving its return type.
109
+ Arbitrary functions, lambdas and unregistered wrappers raise TypeError before
110
+ execution. Busy, ordinary failures, budgets and JobTimeout are propagated.
111
+ If recovery is still unknown, retain that exception and its owner to resume
112
+ later; this helper never replays a user function or loops on recovery.
113
+ """
114
+ if not isinstance(method, MethodType) or method.__func__ not in _REQUEST_METHODS:
115
+ raise TypeError("recover_call requires a registered bound SDK request method")
116
+ try:
117
+ return method(*args, **kwargs)
118
+ except OutcomeUnknown as error:
119
+ return cast(U, error.recover())
@@ -0,0 +1,49 @@
1
+ """Offline public API descriptions; importing or describing never starts a host."""
2
+ from dataclasses import dataclass
3
+ from ._presentation import CompactValue
4
+
5
+ @dataclass(frozen=True,repr=False)
6
+ class ApiDescription(CompactValue):
7
+ 'One installed offline API entry; it describes the client package rather than probing host support.\n\nFields:\npath: Canonical public API path under zipzi.\nkind: Directory entry category such as module, type, callable or value.\nsignature: Rendered public signature; empty for entries without a callable/type signature.\ndocumentation: Semantic docstring for this installed entry.\nfields: Ordered pairs of public field name and semantic description; empty when no value fields are declared.\nchildren: Canonical child entry paths for further bounded describe queries.\nexample: Package-relative workflow/example reference; empty when no example is assigned.'
8
+ __module__='zipzi'
9
+ path:str
10
+ kind:str
11
+ signature:str
12
+ documentation:str
13
+ fields:tuple[tuple[str,str],...]=()
14
+ children:tuple[str,...]=()
15
+ example:str=''
16
+
17
+ def format(self) -> str:
18
+ """Return complete help text without truncation or IO.
19
+
20
+ Use print(description.format()) for full help. repr, str and summary
21
+ remain bounded previews; formatting does not recursively query children.
22
+ """
23
+ lines=[self.path+' ('+self.kind+')']
24
+ if self.signature:lines.extend(('',self.signature))
25
+ if self.documentation:lines.extend(('',self.documentation))
26
+ if self.fields:
27
+ lines.extend(('', 'Fields:'))
28
+ lines.extend(name+': '+text for name,text in self.fields)
29
+ if self.children:
30
+ lines.extend(('', 'Children:', *self.children))
31
+ if self.example:lines.extend(('', 'Example: '+self.example))
32
+ return '\n'.join(lines)
33
+
34
+
35
+ def describe(path:str)->ApiDescription:
36
+ """Look up an exact public path, e.g. Session.meshes.list, without IO or eval.
37
+
38
+ Unknown paths raise KeyError. Short paths are normalized under zipzi.
39
+ The root description links the bundled short guide and executable recipes.
40
+ Use print(describe(path).format()) for complete help; direct printing is a
41
+ bounded preview. Fields and child paths are also available individually.
42
+ This describes the installed API, while runtime.capabilities() describes
43
+ the connected host. No resource is constructed or RPC sent by this query.
44
+ """
45
+ from ._generated.catalog import CATALOG
46
+ if not isinstance(path,str):raise TypeError('Expected a public API path')
47
+ key=path if path=='zipzi' or path.startswith('zipzi.') else 'zipzi.'+path
48
+ raw=CATALOG[key]
49
+ return ApiDescription(key,raw['kind'],raw.get('signature',''),raw.get('documentation',''),tuple(raw.get('fields',())),tuple(raw.get('children',())),raw.get('example',''))
@@ -0,0 +1,61 @@
1
+ """Primitive ownership/shape conversions shared by generated DTO projections."""
2
+ from collections.abc import Callable, Mapping, Sequence
3
+ from typing import TypeVar, cast
4
+ from uuid import UUID
5
+ import math
6
+ from ._values import SessionId
7
+
8
+ T = TypeVar('T')
9
+
10
+ def get(value: object, key: str) -> object:
11
+ if isinstance(value, Mapping): return cast(Mapping[str, object], value)[key]
12
+ return cast(object, getattr(value, key))
13
+
14
+ def text(value: object) -> str:
15
+ if not isinstance(value, str): raise TypeError('Expected text')
16
+ return value
17
+
18
+ def numeric(value: object, kind: str = 'f32') -> float:
19
+ if isinstance(value, bool) or not isinstance(value, (int, float)): raise TypeError('Expected a real number')
20
+ if not math.isfinite(value) or kind == 'f32' and abs(value) > 3.4028234663852886e38: raise ValueError('Expected finite '+kind)
21
+ return float(value)
22
+
23
+ def whole(value: object, kind: str) -> int:
24
+ if type(value) is not int: raise ValueError('Expected '+kind)
25
+ bits = {'u8':8, 'u16':16, 'u32':32, 'u64':64, 'i32':32, 'i64':64, 'usize':64}[kind]
26
+ low, high = (-(1 << (bits-1)), 1 << (bits-1)) if kind.startswith('i') else (0, 1 << bits)
27
+ if not low <= value < high: raise ValueError('Expected '+kind)
28
+ return value
29
+
30
+ def flag(value: object) -> bool:
31
+ if type(value) is not bool: raise TypeError('Expected bool')
32
+ return value
33
+
34
+ def point(value: object) -> tuple[float, float]:
35
+ if isinstance(value, (tuple, list)):
36
+ values=cast(Sequence[object], value)
37
+ if len(values)!=2: raise ValueError('Expected point pair')
38
+ return numeric(values[0]), numeric(values[1])
39
+ return numeric(get(value,'x')), numeric(get(value,'y'))
40
+
41
+ def items(value: object) -> Sequence[object]:
42
+ if not isinstance(value, (list, tuple)): raise TypeError('Expected immutable array input')
43
+ return cast(Sequence[object], value)
44
+
45
+ def array(value: Sequence[T], length: int) -> tuple[T, ...]:
46
+ if len(value)!=length: raise ValueError('Invalid fixed array length')
47
+ return tuple(value)
48
+
49
+ def mapping(value: object) -> Mapping[str, object]:
50
+ if not isinstance(value, Mapping): raise TypeError('Expected mapping')
51
+ result=cast(Mapping[object, object], value)
52
+ if any(not isinstance(key,str) for key in result): raise TypeError('Expected text keys')
53
+ return cast(Mapping[str, object], result)
54
+
55
+ def cursor_guard(runtime: str, session: SessionId | None, sequence: int) -> None:
56
+ if str(UUID(runtime))!=runtime or type(sequence) is not int or not 0<=sequence<1<<64: raise ValueError('Invalid event cursor')
57
+ if session is not None and session.runtime!=runtime: raise ValueError('Cursor mixes Runtime identities')
58
+
59
+
60
+ def external(value: object, cls: type[T], decode: Callable[[object], T]) -> T:
61
+ return value if isinstance(value, cls) else decode(value)
@@ -0,0 +1,148 @@
1
+ """Unix endpoint discovery and ownership transfer to the desktop host."""
2
+ import errno
3
+ import math
4
+ import os
5
+ from pathlib import Path
6
+ import re
7
+ import stat
8
+ import subprocess
9
+ import sys
10
+ import threading
11
+ import time
12
+ from ._errors import SdkFailure
13
+ from .backends.connected import ConnectedBackend
14
+
15
+
16
+ def _private(metadata: os.stat_result, directory: bool) -> None:
17
+ if sys.platform == "win32":
18
+ raise RuntimeError("Unix permission check on Windows")
19
+ valid_kind = stat.S_ISDIR(metadata.st_mode) if directory else stat.S_ISREG(metadata.st_mode)
20
+ if not valid_kind or metadata.st_uid != os.getuid() or metadata.st_mode & 0o077:
21
+ raise PermissionError("Runtime directory/lock must be private, owned by this user and not a symlink")
22
+
23
+
24
+ def _endpoint(path: Path | None, start: bool) -> Path:
25
+ if sys.platform == "win32":
26
+ raise SdkFailure("HOST_UNAVAILABLE", "Local host connections currently require Unix")
27
+ if path is None:
28
+ directory = Path(os.environ.get("ZIPZI_RUNTIME_DIR", f"/tmp/zipzi-runtime-{os.getuid()}"))
29
+ profile = os.environ.get("ZIPZI_RUNTIME_PROFILE", "default")
30
+ path = directory / f"{profile}.sock"
31
+ else:
32
+ directory = path.parent
33
+ profile = path.stem
34
+ if not path.is_absolute() or path.suffix != ".sock" or not re.fullmatch(r"[a-z0-9_-]{1,24}", profile):
35
+ raise ValueError("Endpoint must be an absolute <profile>.sock path with a valid profile")
36
+ if len(os.fsencode(path)) >= 104:
37
+ raise ValueError("Runtime socket path is too long")
38
+ if start:
39
+ try:
40
+ directory.mkdir(mode=0o700)
41
+ except FileExistsError:
42
+ pass
43
+ _private(directory.lstat(), True)
44
+ return path
45
+
46
+
47
+ def _socket(path: Path) -> None:
48
+ if sys.platform == "win32":
49
+ raise RuntimeError("Unix socket check on Windows")
50
+ try:
51
+ metadata = path.lstat()
52
+ except FileNotFoundError:
53
+ return
54
+ if not stat.S_ISSOCK(metadata.st_mode) or metadata.st_uid != os.getuid() or metadata.st_mode & 0o077:
55
+ raise PermissionError("Runtime endpoint must be a private socket owned by this user")
56
+
57
+
58
+ def _host(explicit: Path | None) -> Path:
59
+ configured = os.environ.get("ZIPZI_EDITOR_HOST")
60
+ if explicit is not None or configured:
61
+ candidates = [explicit if explicit is not None else Path(configured or "")]
62
+ elif sys.platform == "darwin":
63
+ candidates = [base / "Zipzi Editor.app/Contents/MacOS/zipzi-editor"
64
+ for base in (Path.home() / "Applications", Path("/Applications"))]
65
+ else:
66
+ candidates = []
67
+ for candidate in candidates:
68
+ if not candidate.is_absolute():
69
+ raise ValueError("Host executable must be absolute")
70
+ if candidate.is_file() and os.access(candidate, os.X_OK):
71
+ return candidate
72
+ raise SdkFailure("HOST_NOT_FOUND", "Set ZIPZI_EDITOR_HOST or pass an absolute host executable")
73
+
74
+
75
+ def _claim(path: Path) -> int | None:
76
+ if sys.platform == "win32":
77
+ raise RuntimeError("Unix ownership claim on Windows")
78
+ import fcntl
79
+ descriptor = os.open(path.with_suffix(".lock"), os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW | os.O_CLOEXEC, 0o600)
80
+ try:
81
+ _private(os.fstat(descriptor), False)
82
+ try:
83
+ fcntl.flock(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB)
84
+ except OSError as error:
85
+ if error.errno in (errno.EAGAIN, errno.EWOULDBLOCK):
86
+ os.close(descriptor)
87
+ return None
88
+ raise
89
+ return descriptor
90
+ except BaseException:
91
+ os.close(descriptor)
92
+ raise
93
+
94
+
95
+ def connect_backend(endpoint: Path | None, *, start: bool, host: Path | None,
96
+ timeout: float, startup_timeout: float, sdk_build: str | None) -> ConnectedBackend:
97
+ for value in (timeout, startup_timeout):
98
+ if isinstance(value, bool) or not math.isfinite(value) or value <= 0:
99
+ raise ValueError("Connection and startup timeouts must be finite and positive")
100
+ if host is not None and not start:
101
+ raise ValueError("A host executable requires start=True")
102
+ if os.name == "nt":
103
+ from ._windows_endpoint import connect
104
+ return connect(endpoint, start=start, host=host, timeout=timeout,
105
+ startup_timeout=startup_timeout, sdk_build=sdk_build)
106
+ path = _endpoint(endpoint, start)
107
+ deadline = time.monotonic() + startup_timeout
108
+ child: subprocess.Popen[bytes] | None = None
109
+
110
+ def attempt() -> ConnectedBackend:
111
+ _socket(path)
112
+ remaining = max(.001, deadline - time.monotonic()) if start else timeout
113
+ return ConnectedBackend(path, timeout, sdk_build=sdk_build, handshake_timeout=min(timeout, remaining))
114
+
115
+ while True:
116
+ try:
117
+ return attempt()
118
+ except (FileNotFoundError, ConnectionRefusedError):
119
+ if not start:
120
+ raise
121
+ if child is not None and child.poll() is not None:
122
+ raise SdkFailure("HOST_START_FAILED", f"Host exited with status {child.returncode} before handshake")
123
+ if time.monotonic() >= deadline:
124
+ raise TimeoutError("Host startup timed out; endpoint ownership is retained by any running host")
125
+ if child is None:
126
+ descriptor = _claim(path)
127
+ if descriptor is not None:
128
+ try:
129
+ # Recheck after winning arbitration, before choosing a host.
130
+ try:
131
+ return attempt()
132
+ except (FileNotFoundError, ConnectionRefusedError):
133
+ pass
134
+ executable = _host(host)
135
+ environment = dict(os.environ, ZIPZI_RUNTIME_DIR=str(path.parent), ZIPZI_RUNTIME_PROFILE=path.stem,
136
+ ZIPZI_RUNTIME_OWNER_FD=str(descriptor))
137
+ # Development session/readiness files belong to their launcher.
138
+ for name in ("ZIPZI_DEV_SESSION_DIR", "ZIPZI_DEV_READY_FILE", "ZIPZI_DEV_PID_FILE"):
139
+ environment.pop(name, None)
140
+ child = subprocess.Popen([str(executable), "--no-window"], env=environment,
141
+ pass_fds=(descriptor,), start_new_session=True, stdin=subprocess.DEVNULL,
142
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
143
+ # Reap without tying the host's lifetime to this client.
144
+ threading.Thread(target=child.wait, name="zipzi-host-reaper", daemon=True).start()
145
+ finally:
146
+ # Never LOCK_UN: the child inherits the same open description.
147
+ os.close(descriptor)
148
+ time.sleep(min(.025, max(0., deadline - time.monotonic())))