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.
- zipzi-0.1.0a2/LICENSE-MIT +21 -0
- zipzi-0.1.0a2/PKG-INFO +40 -0
- zipzi-0.1.0a2/README.md +30 -0
- zipzi-0.1.0a2/THIRD_PARTY_NOTICES.md +33 -0
- zipzi-0.1.0a2/pyproject.toml +16 -0
- zipzi-0.1.0a2/python/zipzi/__init__.py +69 -0
- zipzi-0.1.0a2/python/zipzi/_backend_types.py +3 -0
- zipzi-0.1.0a2/python/zipzi/_calls.py +119 -0
- zipzi-0.1.0a2/python/zipzi/_documentation.py +49 -0
- zipzi-0.1.0a2/python/zipzi/_dto_support.py +61 -0
- zipzi-0.1.0a2/python/zipzi/_endpoint.py +148 -0
- zipzi-0.1.0a2/python/zipzi/_errors.py +131 -0
- zipzi-0.1.0a2/python/zipzi/_extensions.py +38 -0
- zipzi-0.1.0a2/python/zipzi/_facade_support.py +62 -0
- zipzi-0.1.0a2/python/zipzi/_generated/__init__.py +2 -0
- zipzi-0.1.0a2/python/zipzi/_generated/backend_bindings.py +330 -0
- zipzi-0.1.0a2/python/zipzi/_generated/binary.py +4897 -0
- zipzi-0.1.0a2/python/zipzi/_generated/binding_values.py +274 -0
- zipzi-0.1.0a2/python/zipzi/_generated/catalog.py +36651 -0
- zipzi-0.1.0a2/python/zipzi/_generated/discovery.py +92 -0
- zipzi-0.1.0a2/python/zipzi/_generated/event_values.py +199 -0
- zipzi-0.1.0a2/python/zipzi/_generated/facades.py +140 -0
- zipzi-0.1.0a2/python/zipzi/_generated/mesh_ops.py +148 -0
- zipzi-0.1.0a2/python/zipzi/_generated/meshes.py +145 -0
- zipzi-0.1.0a2/python/zipzi/_generated/modeling.py +17008 -0
- zipzi-0.1.0a2/python/zipzi/_generated/parameter_values.py +214 -0
- zipzi-0.1.0a2/python/zipzi/_generated/plan_intents.py +54 -0
- zipzi-0.1.0a2/python/zipzi/_generated/plan_values.py +40 -0
- zipzi-0.1.0a2/python/zipzi/_generated/planning.py +10 -0
- zipzi-0.1.0a2/python/zipzi/_generated/protocols.py +8 -0
- zipzi-0.1.0a2/python/zipzi/_generated/reference_values.py +65 -0
- zipzi-0.1.0a2/python/zipzi/_generated/request_directory.py +103 -0
- zipzi-0.1.0a2/python/zipzi/_generated/resource_backends.py +13 -0
- zipzi-0.1.0a2/python/zipzi/_generated/resource_ops.py +82 -0
- zipzi-0.1.0a2/python/zipzi/_generated/resource_values.py +54 -0
- zipzi-0.1.0a2/python/zipzi/_generated/sampling_values.py +304 -0
- zipzi-0.1.0a2/python/zipzi/_generated/scene.py +915 -0
- zipzi-0.1.0a2/python/zipzi/_jobs.py +151 -0
- zipzi-0.1.0a2/python/zipzi/_lifecycle.py +523 -0
- zipzi-0.1.0a2/python/zipzi/_observation.py +26 -0
- zipzi-0.1.0a2/python/zipzi/_operations.py +44 -0
- zipzi-0.1.0a2/python/zipzi/_owned.py +64 -0
- zipzi-0.1.0a2/python/zipzi/_planning.py +30 -0
- zipzi-0.1.0a2/python/zipzi/_plans.py +49 -0
- zipzi-0.1.0a2/python/zipzi/_presentation.py +46 -0
- zipzi-0.1.0a2/python/zipzi/_preview.py +115 -0
- zipzi-0.1.0a2/python/zipzi/_queries.py +45 -0
- zipzi-0.1.0a2/python/zipzi/_recovery.py +43 -0
- zipzi-0.1.0a2/python/zipzi/_request_methods.py +2 -0
- zipzi-0.1.0a2/python/zipzi/_results.py +45 -0
- zipzi-0.1.0a2/python/zipzi/_spatial.py +32 -0
- zipzi-0.1.0a2/python/zipzi/_transport.py +19 -0
- zipzi-0.1.0a2/python/zipzi/_values.py +195 -0
- zipzi-0.1.0a2/python/zipzi/_windows_endpoint.py +95 -0
- zipzi-0.1.0a2/python/zipzi/_windows_pipe.py +171 -0
- zipzi-0.1.0a2/python/zipzi/animation.py +5 -0
- zipzi-0.1.0a2/python/zipzi/backends/__init__.py +82 -0
- zipzi-0.1.0a2/python/zipzi/backends/_native_types.py +80 -0
- zipzi-0.1.0a2/python/zipzi/backends/_transfers.py +192 -0
- zipzi-0.1.0a2/python/zipzi/backends/connected.py +301 -0
- zipzi-0.1.0a2/python/zipzi/backends/local.py +71 -0
- zipzi-0.1.0a2/python/zipzi/bindings.py +5 -0
- zipzi-0.1.0a2/python/zipzi/buffers.py +98 -0
- zipzi-0.1.0a2/python/zipzi/docs/editor_handoff.py +52 -0
- zipzi-0.1.0a2/python/zipzi/docs/guide.md +290 -0
- zipzi-0.1.0a2/python/zipzi/docs/recipes.py +138 -0
- zipzi-0.1.0a2/python/zipzi/docs/reference.md +26968 -0
- zipzi-0.1.0a2/python/zipzi/events.py +103 -0
- zipzi-0.1.0a2/python/zipzi/inspection.py +60 -0
- zipzi-0.1.0a2/python/zipzi/mesh_generation.py +83 -0
- zipzi-0.1.0a2/python/zipzi/meshes.py +6 -0
- zipzi-0.1.0a2/python/zipzi/modeling.py +19 -0
- zipzi-0.1.0a2/python/zipzi/observation.py +4 -0
- zipzi-0.1.0a2/python/zipzi/ops.py +13 -0
- zipzi-0.1.0a2/python/zipzi/parameters.py +5 -0
- zipzi-0.1.0a2/python/zipzi/py.typed +0 -0
- zipzi-0.1.0a2/python/zipzi/references.py +138 -0
- zipzi-0.1.0a2/python/zipzi/resources.py +133 -0
- zipzi-0.1.0a2/python/zipzi/sampling.py +20 -0
- zipzi-0.1.0a2/python/zipzi/scene.py +4 -0
- 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.
|
zipzi-0.1.0a2/README.md
ADDED
|
@@ -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,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())))
|