pardesline 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.
@@ -0,0 +1,22 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ .venv/
11
+ venv/
12
+ .mypy_cache/
13
+
14
+ # Local
15
+ .env
16
+ *.log
17
+ .DS_Store
18
+
19
+ # Never commit credentials / recovery codes
20
+ *Recovery-Codes*
21
+ *recovery*codes*
22
+ token
@@ -0,0 +1,23 @@
1
+ PardesLine Python SDK — Proprietary License
2
+ Copyright (c) 2026 PardesLine. All rights reserved.
3
+
4
+ This software (the "SDK") is a client library for the PardesLine platform. It
5
+ is provided to let you access the PardesLine service through its public API.
6
+
7
+ Permission is granted to install and use the SDK solely to interact with the
8
+ PardesLine service, subject to the PardesLine Terms of Service
9
+ (https://appbeta.pardesline.com). You may not copy, modify, redistribute,
10
+ sublicense, sell, or create derivative works of the SDK, in whole or in part,
11
+ or reuse its code outside of accessing the PardesLine service, without the
12
+ prior written permission of PardesLine.
13
+
14
+ Use of the PardesLine service through this SDK is governed by the PardesLine
15
+ Terms of Service and the applicable plan and quotas. The service is commercial:
16
+ free up to the plan quotas, paid beyond them.
17
+
18
+ THE SDK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
19
+ INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
20
+ PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
21
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
22
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
23
+ CONNECTION WITH THE SDK OR THE USE OR OTHER DEALINGS IN THE SDK.
@@ -0,0 +1,119 @@
1
+ Metadata-Version: 2.5
2
+ Name: pardesline
3
+ Version: 0.1.0
4
+ Summary: Transfer files between PardesLine accounts in a few lines — bytes go straight to cloud storage, the API only hands out the addresses.
5
+ Project-URL: Homepage, https://appbeta.pardesline.com
6
+ Project-URL: Repository, https://github.com/1904jonathan/pardesline-python
7
+ Author: PardesLine
8
+ License: LicenseRef-PardesLine-Proprietary
9
+ License-File: LICENSE
10
+ Keywords: 3d,cloud-storage,dicom,file-transfer,mesh,pardesline,point-cloud
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: License :: Other/Proprietary License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Requires-Python: >=3.9
17
+ Requires-Dist: httpx<1,>=0.27
18
+ Requires-Dist: pydantic<3,>=2
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest>=8; extra == 'dev'
21
+ Requires-Dist: respx>=0.21; extra == 'dev'
22
+ Requires-Dist: ruff>=0.5; extra == 'dev'
23
+ Description-Content-Type: text/markdown
24
+
25
+ # pardesline
26
+
27
+ Transfer files between PardesLine accounts in a few lines of Python. The bytes
28
+ go **straight to cloud storage** — the API only hands out the upload/download
29
+ addresses — so multi-gigabyte 3D scans, DICOM folders, `.npy`, `.json`,
30
+ anything, move without passing through the server.
31
+
32
+ ```bash
33
+ pip install pardesline
34
+ ```
35
+
36
+ ## Collaboration in 10 lines
37
+
38
+ ```python
39
+ import pardesline as pl
40
+
41
+ pl.login() # email + 6-digit code, once; key stored in ~/.pardesline/token
42
+
43
+ obj = pl.upload("scan.ply") # uploaded straight to your PardesLine storage
44
+ pl.share(obj, to="bob@lab.org") # Bob now sees it in pl.inbox()
45
+ # ...or both at once:
46
+ pl.send("scan.ply", to="bob@lab.org")
47
+
48
+ # on Bob's machine
49
+ obj = pl.read("alice@lab.org/scan.ply") # what Alice shared with me (or a file id)
50
+ obj.download("scan.ply") # streamed directly from storage
51
+ ```
52
+
53
+ Send a whole folder — it is zipped on the way out and can be unpacked on the way in:
54
+
55
+ ```python
56
+ pl.send("patient_ct/", to="bob@lab.org") # -> patient_ct.zip
57
+ pl.read("alice@lab.org/patient_ct.zip").download("ct/", extract=True)
58
+ ```
59
+
60
+ ## Logging in
61
+
62
+ `pl.login()` (no argument) prompts for your email, sends a 6-digit code, and
63
+ stores a long-lived key at `~/.pardesline/token`. Alternatives:
64
+
65
+ ```python
66
+ pl.login(token="pl_...") # a key copied from the website (Hugging-Face style)
67
+ ```
68
+ ```bash
69
+ export PARDESLINE_API_KEY=pl_... # CI / notebooks (no file written)
70
+ ```
71
+
72
+ The same key works with the REST API (`X-API-Key: pl_…`) and the MCP server.
73
+
74
+ ## What you can do
75
+
76
+ | | |
77
+ |---|---|
78
+ | `pl.upload(path)` / `pl.upload_bytes(data, name)` | Upload a file, folder (zipped) or raw bytes → `RemoteObject` |
79
+ | `pl.send(path, to=email)` | Upload **and** share in one call |
80
+ | `pl.read(ref)` | Resolve `"owner@email/name"` or a file id → `RemoteObject` |
81
+ | `pl.inbox()` / `pl.list_files(scope="mine\|shared\|all")` | What was shared with you / your files |
82
+ | `obj.download(path, extract=False)` · `obj.read_bytes()` · `obj.download_url()` | Get the bytes / a shareable link |
83
+ | `pl.share(obj, to=email)` · `obj.share(to=email)` · `pl.delete(obj)` | Manage sharing and deletion |
84
+ | `pl.me()` / `pl.whoami()` | Your plan, quotas and usage |
85
+
86
+ Command line:
87
+
88
+ ```bash
89
+ pardesline login
90
+ pardesline send scan.ply bob@lab.org
91
+ pardesline inbox
92
+ pardesline download "alice@lab.org/scan.ply" out.ply
93
+ ```
94
+
95
+ ## Free with quotas, paid beyond
96
+
97
+ PardesLine is a commercial service: every account is **free up to the plan
98
+ quotas** (storage, transfers, API calls), and paid beyond them. When a quota is
99
+ reached the SDK raises `pardesline.QuotaExceeded` carrying the numbers and the
100
+ upgrade link:
101
+
102
+ ```python
103
+ try:
104
+ pl.upload("huge.vti")
105
+ except pl.QuotaExceeded as e:
106
+ print(e.quota, e.used, "/", e.limit, "->", e.upgrade_url)
107
+ ```
108
+
109
+ Other typed errors: `AuthError`, `Forbidden`, `NotFound`, `TooLarge`,
110
+ `RateLimited`, `PlatformLocked`, `TransferError`.
111
+
112
+ ## Notes
113
+
114
+ - Files up to 5 GiB each. Any file type transfers; the platform's processing
115
+ modules still expect 3D formats.
116
+ - A file you receive is readable only through the share; the owner can revoke it
117
+ at any time.
118
+ - This SDK is proprietary (see `LICENSE`); use is governed by the PardesLine
119
+ Terms of Service.
@@ -0,0 +1,95 @@
1
+ # pardesline
2
+
3
+ Transfer files between PardesLine accounts in a few lines of Python. The bytes
4
+ go **straight to cloud storage** — the API only hands out the upload/download
5
+ addresses — so multi-gigabyte 3D scans, DICOM folders, `.npy`, `.json`,
6
+ anything, move without passing through the server.
7
+
8
+ ```bash
9
+ pip install pardesline
10
+ ```
11
+
12
+ ## Collaboration in 10 lines
13
+
14
+ ```python
15
+ import pardesline as pl
16
+
17
+ pl.login() # email + 6-digit code, once; key stored in ~/.pardesline/token
18
+
19
+ obj = pl.upload("scan.ply") # uploaded straight to your PardesLine storage
20
+ pl.share(obj, to="bob@lab.org") # Bob now sees it in pl.inbox()
21
+ # ...or both at once:
22
+ pl.send("scan.ply", to="bob@lab.org")
23
+
24
+ # on Bob's machine
25
+ obj = pl.read("alice@lab.org/scan.ply") # what Alice shared with me (or a file id)
26
+ obj.download("scan.ply") # streamed directly from storage
27
+ ```
28
+
29
+ Send a whole folder — it is zipped on the way out and can be unpacked on the way in:
30
+
31
+ ```python
32
+ pl.send("patient_ct/", to="bob@lab.org") # -> patient_ct.zip
33
+ pl.read("alice@lab.org/patient_ct.zip").download("ct/", extract=True)
34
+ ```
35
+
36
+ ## Logging in
37
+
38
+ `pl.login()` (no argument) prompts for your email, sends a 6-digit code, and
39
+ stores a long-lived key at `~/.pardesline/token`. Alternatives:
40
+
41
+ ```python
42
+ pl.login(token="pl_...") # a key copied from the website (Hugging-Face style)
43
+ ```
44
+ ```bash
45
+ export PARDESLINE_API_KEY=pl_... # CI / notebooks (no file written)
46
+ ```
47
+
48
+ The same key works with the REST API (`X-API-Key: pl_…`) and the MCP server.
49
+
50
+ ## What you can do
51
+
52
+ | | |
53
+ |---|---|
54
+ | `pl.upload(path)` / `pl.upload_bytes(data, name)` | Upload a file, folder (zipped) or raw bytes → `RemoteObject` |
55
+ | `pl.send(path, to=email)` | Upload **and** share in one call |
56
+ | `pl.read(ref)` | Resolve `"owner@email/name"` or a file id → `RemoteObject` |
57
+ | `pl.inbox()` / `pl.list_files(scope="mine\|shared\|all")` | What was shared with you / your files |
58
+ | `obj.download(path, extract=False)` · `obj.read_bytes()` · `obj.download_url()` | Get the bytes / a shareable link |
59
+ | `pl.share(obj, to=email)` · `obj.share(to=email)` · `pl.delete(obj)` | Manage sharing and deletion |
60
+ | `pl.me()` / `pl.whoami()` | Your plan, quotas and usage |
61
+
62
+ Command line:
63
+
64
+ ```bash
65
+ pardesline login
66
+ pardesline send scan.ply bob@lab.org
67
+ pardesline inbox
68
+ pardesline download "alice@lab.org/scan.ply" out.ply
69
+ ```
70
+
71
+ ## Free with quotas, paid beyond
72
+
73
+ PardesLine is a commercial service: every account is **free up to the plan
74
+ quotas** (storage, transfers, API calls), and paid beyond them. When a quota is
75
+ reached the SDK raises `pardesline.QuotaExceeded` carrying the numbers and the
76
+ upgrade link:
77
+
78
+ ```python
79
+ try:
80
+ pl.upload("huge.vti")
81
+ except pl.QuotaExceeded as e:
82
+ print(e.quota, e.used, "/", e.limit, "->", e.upgrade_url)
83
+ ```
84
+
85
+ Other typed errors: `AuthError`, `Forbidden`, `NotFound`, `TooLarge`,
86
+ `RateLimited`, `PlatformLocked`, `TransferError`.
87
+
88
+ ## Notes
89
+
90
+ - Files up to 5 GiB each. Any file type transfers; the platform's processing
91
+ modules still expect 3D formats.
92
+ - A file you receive is readable only through the share; the owner can revoke it
93
+ at any time.
94
+ - This SDK is proprietary (see `LICENSE`); use is governed by the PardesLine
95
+ Terms of Service.
@@ -0,0 +1,57 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "pardesline"
7
+ dynamic = ["version"]
8
+ description = "Transfer files between PardesLine accounts in a few lines — bytes go straight to cloud storage, the API only hands out the addresses."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "LicenseRef-PardesLine-Proprietary" }
12
+ authors = [{ name = "PardesLine" }]
13
+ keywords = ["pardesline", "file-transfer", "3d", "point-cloud", "mesh", "dicom", "cloud-storage"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: Other/Proprietary License",
17
+ "Operating System :: OS Independent",
18
+ "Intended Audience :: Developers",
19
+ "Intended Audience :: Science/Research",
20
+ ]
21
+ dependencies = [
22
+ "httpx>=0.27,<1",
23
+ "pydantic>=2,<3",
24
+ ]
25
+
26
+ [project.optional-dependencies]
27
+ dev = ["pytest>=8", "respx>=0.21", "ruff>=0.5"]
28
+
29
+ [project.urls]
30
+ Homepage = "https://appbeta.pardesline.com"
31
+ Repository = "https://github.com/1904jonathan/pardesline-python"
32
+
33
+ [project.scripts]
34
+ pardesline = "pardesline.cli:main"
35
+
36
+ [tool.hatch.version]
37
+ path = "src/pardesline/__init__.py"
38
+
39
+ [tool.hatch.build.targets.wheel]
40
+ packages = ["src/pardesline"]
41
+
42
+ [tool.hatch.build.targets.sdist]
43
+ include = ["src/pardesline", "README.md", "LICENSE"]
44
+
45
+ [tool.ruff]
46
+ line-length = 110
47
+ target-version = "py39"
48
+
49
+ [tool.ruff.lint]
50
+ # We keep typing.Optional/Union (works on 3.9 at runtime, no typing_extensions
51
+ # dependency for Self) and don't want the pyupgrade union rewrites. BLE001 /
52
+ # SIM115 / SIM117 flag deliberate best-effort try/except and temp-file handling.
53
+ ignore = ["UP006", "UP007", "UP035", "UP045", "PYI034", "BLE001", "SIM115", "SIM117", "S110"]
54
+
55
+ [tool.ruff.lint.per-file-ignores]
56
+ # Tests assert on attribute-access side effects (B018) and use small helpers.
57
+ "tests/*" = ["B018", "F841"]
@@ -0,0 +1,173 @@
1
+ """PardesLine — transfer files between accounts in a few lines.
2
+
3
+ import pardesline as pl
4
+ pl.login()
5
+ obj = pl.upload("scan.ply")
6
+ pl.share(obj, to="bob@lab.org")
7
+
8
+ # bob:
9
+ pl.read("alice@lab.org/scan.ply").download("scan.ply")
10
+
11
+ The module-level helpers use a default client built lazily from the stored
12
+ credentials (PARDESLINE_API_KEY or ~/.pardesline/token). For explicit control,
13
+ use the Client class.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from typing import Optional
19
+
20
+ __version__ = "0.1.0"
21
+
22
+ from .client import Client
23
+ from .errors import (
24
+ AuthError,
25
+ Conflict,
26
+ Forbidden,
27
+ GpuAddonRequired,
28
+ NotFound,
29
+ PardesLineError,
30
+ PlatformLocked,
31
+ QuotaExceeded,
32
+ RateLimited,
33
+ TooLarge,
34
+ TransferError,
35
+ )
36
+ from .objects import RemoteObject
37
+
38
+ __all__ = [
39
+ "AuthError",
40
+ "Client",
41
+ "Conflict",
42
+ "Forbidden",
43
+ "GpuAddonRequired",
44
+ "NotFound",
45
+ "PardesLineError",
46
+ "PlatformLocked",
47
+ "QuotaExceeded",
48
+ "RateLimited",
49
+ "RemoteObject",
50
+ "TooLarge",
51
+ "TransferError",
52
+ "delete",
53
+ "download",
54
+ "gpu",
55
+ "inbox",
56
+ "list_files",
57
+ "login",
58
+ "logout",
59
+ "me",
60
+ "np",
61
+ "o3d",
62
+ "pv",
63
+ "read",
64
+ "send",
65
+ "share",
66
+ "sitk",
67
+ "upload",
68
+ "upload_bytes",
69
+ "view",
70
+ "view_registration",
71
+ "whoami",
72
+ ]
73
+
74
+ _default: Optional[Client] = None
75
+
76
+
77
+ def _client() -> Client:
78
+ global _default
79
+ if _default is None:
80
+ _default = Client()
81
+ return _default
82
+
83
+
84
+ def login(token: Optional[str] = None, *, email: Optional[str] = None, save: bool = True) -> Client:
85
+ """Log in (terminal email+code flow, or with a pl_ key). Returns the client."""
86
+ global _default
87
+ _default = Client(token=token) if token else Client()
88
+ return _default.login(token=token, email=email, save=save)
89
+
90
+
91
+ def logout() -> None:
92
+ _client().logout()
93
+
94
+
95
+ def whoami() -> Optional[str]:
96
+ return _client().whoami()
97
+
98
+
99
+ def me():
100
+ return _client().me()
101
+
102
+
103
+ def upload(path, *, name=None, project=None, workspace=None) -> RemoteObject:
104
+ return _client().upload(path, name=name, project=project, workspace=workspace)
105
+
106
+
107
+ def upload_bytes(data: bytes, name: str, *, project=None) -> RemoteObject:
108
+ return _client().upload_bytes(data, name, project=project)
109
+
110
+
111
+ def send(path, to: str, *, name=None, message=None) -> RemoteObject:
112
+ """Upload a local file and hand it to another account in one call."""
113
+ return _client().send(path, to=to, name=name, message=message)
114
+
115
+
116
+ def read(ref: str) -> RemoteObject:
117
+ return _client().read(ref)
118
+
119
+
120
+ def download(ref: str, path=None, *, extract: bool = False):
121
+ return _client().download(ref, path, extract=extract)
122
+
123
+
124
+ def share(obj, *, to: str, message: Optional[str] = None):
125
+ return _client().share(obj, to=to, message=message)
126
+
127
+
128
+ def inbox(*, limit: int = 50):
129
+ return _client().inbox(limit=limit)
130
+
131
+
132
+ def list_files(scope: str = "mine", *, limit: int = 50):
133
+ return _client().list(scope, limit=limit)
134
+
135
+
136
+ def delete(obj) -> None:
137
+ _client().delete(obj)
138
+
139
+
140
+ def view(*objs):
141
+ return _client().view(*objs)
142
+
143
+
144
+ def view_registration(fixed, moving=None, *, method: str = "rigid"):
145
+ return _client().view_registration(fixed, moving, method=method)
146
+
147
+
148
+ class _GpuProxy:
149
+ def __getattr__(self, name):
150
+ return getattr(_client().gpu, name)
151
+
152
+
153
+ gpu = _GpuProxy()
154
+
155
+
156
+ class _LibProxy:
157
+ """Module-level pl.o3d / pl.pv / pl.sitk / pl.np — the faithful, server-side
158
+ mirrors of open3d / pyvista / SimpleITK / numpy (bound to the default client)."""
159
+
160
+ def __init__(self, attr: str):
161
+ self._attr = attr
162
+
163
+ def __getattr__(self, name: str):
164
+ return getattr(getattr(_client(), self._attr), name)
165
+
166
+ def __call__(self, *args, **kwargs):
167
+ return getattr(_client(), self._attr)(*args, **kwargs)
168
+
169
+
170
+ o3d = _LibProxy("o3d")
171
+ pv = _LibProxy("pv")
172
+ sitk = _LibProxy("sitk")
173
+ np = _LibProxy("np")
@@ -0,0 +1,89 @@
1
+ """Credential resolution and the terminal login flow.
2
+
3
+ Precedence (highest first):
4
+ 1. an explicit token passed to Client(token=...) / login(token=...)
5
+ 2. the PARDESLINE_API_KEY environment variable (CI, notebooks)
6
+ 3. the key saved at ~/.pardesline/token (mode 0600) by a previous login()
7
+
8
+ login() with no argument is the Hugging-Face-style terminal flow: email →
9
+ 6-digit code → the SDK mints a long-lived pl_ key with that short session and
10
+ stores it; the session token is discarded.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import os
16
+ import socket
17
+ import stat
18
+ from pathlib import Path
19
+ from typing import Optional
20
+
21
+ ENV_VAR = "PARDESLINE_API_KEY"
22
+ ENV_BASE_URL = "PARDESLINE_BASE_URL"
23
+ DEFAULT_BASE_URL = "https://appbeta.pardesline.com"
24
+
25
+
26
+ def token_path() -> Path:
27
+ override = os.environ.get("PARDESLINE_TOKEN_PATH")
28
+ if override:
29
+ return Path(override)
30
+ return Path.home() / ".pardesline" / "token"
31
+
32
+
33
+ def load_saved_token() -> Optional[str]:
34
+ p = token_path()
35
+ try:
36
+ if p.exists():
37
+ tok = p.read_text(encoding="utf-8").strip()
38
+ return tok or None
39
+ except OSError:
40
+ pass
41
+ return None
42
+
43
+
44
+ def save_token(token: str) -> Path:
45
+ p = token_path()
46
+ p.parent.mkdir(parents=True, exist_ok=True)
47
+ p.write_text(token.strip() + "\n", encoding="utf-8")
48
+ try:
49
+ os.chmod(p, stat.S_IRUSR | stat.S_IWUSR) # 0600 (best-effort on Windows)
50
+ except OSError:
51
+ pass
52
+ return p
53
+
54
+
55
+ def clear_saved_token() -> None:
56
+ try:
57
+ token_path().unlink(missing_ok=True) # type: ignore[call-arg]
58
+ except TypeError: # Python 3.7/3.8 have no missing_ok
59
+ try:
60
+ token_path().unlink()
61
+ except OSError:
62
+ pass
63
+ except OSError:
64
+ pass
65
+
66
+
67
+ def resolve_token(explicit: Optional[str] = None) -> Optional[str]:
68
+ if explicit:
69
+ return explicit.strip()
70
+ env = os.environ.get(ENV_VAR)
71
+ if env:
72
+ return env.strip()
73
+ return load_saved_token()
74
+
75
+
76
+ def resolve_base_url(explicit: Optional[str] = None) -> str:
77
+ return (explicit or os.environ.get(ENV_BASE_URL) or DEFAULT_BASE_URL).rstrip("/")
78
+
79
+
80
+ def default_key_name() -> str:
81
+ try:
82
+ host = socket.gethostname() or "device"
83
+ except Exception:
84
+ host = "device"
85
+ return f"{host} (pardesline-python)"
86
+
87
+
88
+ def prompt(msg: str) -> str:
89
+ return input(msg).strip()