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.
- pardesline-0.1.0/.gitignore +22 -0
- pardesline-0.1.0/LICENSE +23 -0
- pardesline-0.1.0/PKG-INFO +119 -0
- pardesline-0.1.0/README.md +95 -0
- pardesline-0.1.0/pyproject.toml +57 -0
- pardesline-0.1.0/src/pardesline/__init__.py +173 -0
- pardesline-0.1.0/src/pardesline/_auth.py +89 -0
- pardesline-0.1.0/src/pardesline/_catalog.py +136 -0
- pardesline-0.1.0/src/pardesline/_http.py +126 -0
- pardesline-0.1.0/src/pardesline/cli.py +84 -0
- pardesline-0.1.0/src/pardesline/client.py +308 -0
- pardesline-0.1.0/src/pardesline/errors.py +113 -0
- pardesline-0.1.0/src/pardesline/jobs.py +80 -0
- pardesline-0.1.0/src/pardesline/models.py +118 -0
- pardesline-0.1.0/src/pardesline/objects.py +220 -0
- pardesline-0.1.0/src/pardesline/py.typed +0 -0
- pardesline-0.1.0/src/pardesline/remote.py +294 -0
- pardesline-0.1.0/src/pardesline/resources.py +201 -0
|
@@ -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
|
pardesline-0.1.0/LICENSE
ADDED
|
@@ -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()
|