olive-compute 0.1.1__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.
- olive_compute-0.1.1/.gitignore +63 -0
- olive_compute-0.1.1/PKG-INFO +126 -0
- olive_compute-0.1.1/README.md +113 -0
- olive_compute-0.1.1/olive/__init__.py +16 -0
- olive_compute-0.1.1/olive/cli.py +163 -0
- olive_compute-0.1.1/olive/client.py +874 -0
- olive_compute-0.1.1/olive/compute.py +295 -0
- olive_compute-0.1.1/pyproject.toml +26 -0
- olive_compute-0.1.1/tests/__init__.py +0 -0
- olive_compute-0.1.1/tests/test_async_client.py +257 -0
- olive_compute-0.1.1/tests/test_cli.py +140 -0
- olive_compute-0.1.1/tests/test_client.py +570 -0
- olive_compute-0.1.1/tests/test_compute.py +316 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Environment
|
|
2
|
+
.env
|
|
3
|
+
.env.local
|
|
4
|
+
|
|
5
|
+
# Rust
|
|
6
|
+
target/
|
|
7
|
+
*.pdb
|
|
8
|
+
|
|
9
|
+
# Python
|
|
10
|
+
__pycache__/
|
|
11
|
+
*.py[cod]
|
|
12
|
+
.venv/
|
|
13
|
+
venv/
|
|
14
|
+
.pytest_cache/
|
|
15
|
+
.mypy_cache/
|
|
16
|
+
*.egg-info/
|
|
17
|
+
|
|
18
|
+
# Node
|
|
19
|
+
node_modules/
|
|
20
|
+
dist/
|
|
21
|
+
build/
|
|
22
|
+
.next/
|
|
23
|
+
|
|
24
|
+
# Tauri runtime assets are downloaded during app CI builds.
|
|
25
|
+
app/src-tauri/resources/runtime/*
|
|
26
|
+
!app/src-tauri/resources/runtime/README.md
|
|
27
|
+
|
|
28
|
+
# Catalog staged into job-api build context (source of truth is /config/models.yaml)
|
|
29
|
+
services/job-api/config/
|
|
30
|
+
|
|
31
|
+
# IDE
|
|
32
|
+
.vscode/
|
|
33
|
+
.idea/
|
|
34
|
+
*.swp
|
|
35
|
+
.DS_Store
|
|
36
|
+
|
|
37
|
+
# Local data volumes
|
|
38
|
+
postgres_data/
|
|
39
|
+
redis_data/
|
|
40
|
+
grafana_data/
|
|
41
|
+
rabbitmq_data/
|
|
42
|
+
minio_data/
|
|
43
|
+
|
|
44
|
+
# Generated certs and keys
|
|
45
|
+
*.pem
|
|
46
|
+
*.key
|
|
47
|
+
*.crt
|
|
48
|
+
certs/
|
|
49
|
+
!**/example*.pem
|
|
50
|
+
|
|
51
|
+
# CDK
|
|
52
|
+
cdk.out/
|
|
53
|
+
infrastructure/node_modules/
|
|
54
|
+
|
|
55
|
+
# Benchmark outputs
|
|
56
|
+
*.benchmark.json
|
|
57
|
+
reports/
|
|
58
|
+
|
|
59
|
+
# Internal task orchestrator local state
|
|
60
|
+
.task-orchestrator/
|
|
61
|
+
|
|
62
|
+
# Local Claude Code project state
|
|
63
|
+
.claude/
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: olive-compute
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Python client for the Olive distributed AI compute platform
|
|
5
|
+
Project-URL: Homepage, https://olivecompute.com
|
|
6
|
+
Project-URL: Documentation, https://olivecompute.com/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/yotammos/olive
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: ai,compute,distributed,embeddings,inference
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Requires-Dist: httpx>=0.27
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# Olive Python SDK
|
|
15
|
+
|
|
16
|
+
Distributed AI compute — embeddings and inference — with one import.
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install olive-compute
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quickstart
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from olive import OliveClient
|
|
26
|
+
|
|
27
|
+
client = OliveClient(api_key="olv_...")
|
|
28
|
+
|
|
29
|
+
# Embed text — uses the default embeddings model
|
|
30
|
+
vectors = client.embeddings(["hello world", "olive compute"])
|
|
31
|
+
print(vectors[0][:4]) # [0.0521, -0.1234, ...]
|
|
32
|
+
|
|
33
|
+
# Run inference — uses the default chat model
|
|
34
|
+
reply = client.inference("What is a neural network?", max_tokens=128)
|
|
35
|
+
print(reply)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Choosing a model
|
|
39
|
+
|
|
40
|
+
Olive supports a catalog of curated open-source models. Browse them at
|
|
41
|
+
[olivecompute.com/models](https://olivecompute.com/models) or programmatically:
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
# List all available chat models
|
|
45
|
+
for m in client.list_models(modality="chat"):
|
|
46
|
+
print(m["id"], "—", m["pricing"]["input_per_1m_tokens_usd"], "/1M tokens")
|
|
47
|
+
|
|
48
|
+
# Get one model's full record
|
|
49
|
+
m = client.get_model("meta/llama-3.1-8b-instruct")
|
|
50
|
+
print(m["description"])
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Pass `model=` to any inference call to pin a specific model:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
reply = client.inference(
|
|
57
|
+
"Write a Python function to reverse a list.",
|
|
58
|
+
model="qwen/qwen-2.5-coder-7b",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
vectors = client.embeddings(
|
|
62
|
+
["semantic search query"],
|
|
63
|
+
model="baai/bge-large-en-v1.5",
|
|
64
|
+
)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
If `model=` is omitted, Olive picks the default (featured) model for the workload.
|
|
68
|
+
|
|
69
|
+
## Authentication
|
|
70
|
+
|
|
71
|
+
Get an API key from [provider.olivecompute.com](https://provider.olivecompute.com) → Settings → API Keys.
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
# API key (recommended)
|
|
75
|
+
client = OliveClient(api_key="olv_...")
|
|
76
|
+
|
|
77
|
+
# Email + password (issues a short-lived token automatically)
|
|
78
|
+
client = OliveClient(email="you@example.com", password="...")
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Compute tiers
|
|
82
|
+
|
|
83
|
+
| Tier | CPU | RAM | Use case |
|
|
84
|
+
|------|-----|-----|----------|
|
|
85
|
+
| `"light"` | 1 core | 2 GB | Embeddings, small inputs |
|
|
86
|
+
| `"medium"` | 2 cores | 4 GB | Standard inference (default) |
|
|
87
|
+
| `"heavy"` | 4 cores | 8 GB | Long context, large batches |
|
|
88
|
+
|
|
89
|
+
## Async jobs
|
|
90
|
+
|
|
91
|
+
For long-running workloads, submit and poll separately:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
job = client.submit_job(
|
|
95
|
+
workload_type="inference",
|
|
96
|
+
input_data='{"prompt": "Write a haiku", "max_tokens": 64}',
|
|
97
|
+
model="meta/llama-3.1-8b-instruct", # optional — default chat model otherwise
|
|
98
|
+
compute="medium",
|
|
99
|
+
)
|
|
100
|
+
print(job.id) # e3b2a1c0-...
|
|
101
|
+
print(job.status) # "running"
|
|
102
|
+
|
|
103
|
+
result = job.wait(timeout=120)
|
|
104
|
+
print(result["output_data"])
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Error handling
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from olive import OliveClient, AuthError, JobError
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
client = OliveClient(api_key="bad_key")
|
|
114
|
+
vectors = client.embeddings(["test"])
|
|
115
|
+
except AuthError:
|
|
116
|
+
print("Check your API key")
|
|
117
|
+
except JobError as e:
|
|
118
|
+
print(f"Job failed: {e}")
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Context manager
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
with OliveClient(api_key="olv_...") as client:
|
|
125
|
+
vectors = client.embeddings(["hello"])
|
|
126
|
+
```
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Olive Python SDK
|
|
2
|
+
|
|
3
|
+
Distributed AI compute — embeddings and inference — with one import.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install olive-compute
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Quickstart
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
from olive import OliveClient
|
|
13
|
+
|
|
14
|
+
client = OliveClient(api_key="olv_...")
|
|
15
|
+
|
|
16
|
+
# Embed text — uses the default embeddings model
|
|
17
|
+
vectors = client.embeddings(["hello world", "olive compute"])
|
|
18
|
+
print(vectors[0][:4]) # [0.0521, -0.1234, ...]
|
|
19
|
+
|
|
20
|
+
# Run inference — uses the default chat model
|
|
21
|
+
reply = client.inference("What is a neural network?", max_tokens=128)
|
|
22
|
+
print(reply)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Choosing a model
|
|
26
|
+
|
|
27
|
+
Olive supports a catalog of curated open-source models. Browse them at
|
|
28
|
+
[olivecompute.com/models](https://olivecompute.com/models) or programmatically:
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
# List all available chat models
|
|
32
|
+
for m in client.list_models(modality="chat"):
|
|
33
|
+
print(m["id"], "—", m["pricing"]["input_per_1m_tokens_usd"], "/1M tokens")
|
|
34
|
+
|
|
35
|
+
# Get one model's full record
|
|
36
|
+
m = client.get_model("meta/llama-3.1-8b-instruct")
|
|
37
|
+
print(m["description"])
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Pass `model=` to any inference call to pin a specific model:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
reply = client.inference(
|
|
44
|
+
"Write a Python function to reverse a list.",
|
|
45
|
+
model="qwen/qwen-2.5-coder-7b",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
vectors = client.embeddings(
|
|
49
|
+
["semantic search query"],
|
|
50
|
+
model="baai/bge-large-en-v1.5",
|
|
51
|
+
)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
If `model=` is omitted, Olive picks the default (featured) model for the workload.
|
|
55
|
+
|
|
56
|
+
## Authentication
|
|
57
|
+
|
|
58
|
+
Get an API key from [provider.olivecompute.com](https://provider.olivecompute.com) → Settings → API Keys.
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
# API key (recommended)
|
|
62
|
+
client = OliveClient(api_key="olv_...")
|
|
63
|
+
|
|
64
|
+
# Email + password (issues a short-lived token automatically)
|
|
65
|
+
client = OliveClient(email="you@example.com", password="...")
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Compute tiers
|
|
69
|
+
|
|
70
|
+
| Tier | CPU | RAM | Use case |
|
|
71
|
+
|------|-----|-----|----------|
|
|
72
|
+
| `"light"` | 1 core | 2 GB | Embeddings, small inputs |
|
|
73
|
+
| `"medium"` | 2 cores | 4 GB | Standard inference (default) |
|
|
74
|
+
| `"heavy"` | 4 cores | 8 GB | Long context, large batches |
|
|
75
|
+
|
|
76
|
+
## Async jobs
|
|
77
|
+
|
|
78
|
+
For long-running workloads, submit and poll separately:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
job = client.submit_job(
|
|
82
|
+
workload_type="inference",
|
|
83
|
+
input_data='{"prompt": "Write a haiku", "max_tokens": 64}',
|
|
84
|
+
model="meta/llama-3.1-8b-instruct", # optional — default chat model otherwise
|
|
85
|
+
compute="medium",
|
|
86
|
+
)
|
|
87
|
+
print(job.id) # e3b2a1c0-...
|
|
88
|
+
print(job.status) # "running"
|
|
89
|
+
|
|
90
|
+
result = job.wait(timeout=120)
|
|
91
|
+
print(result["output_data"])
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Error handling
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from olive import OliveClient, AuthError, JobError
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
client = OliveClient(api_key="bad_key")
|
|
101
|
+
vectors = client.embeddings(["test"])
|
|
102
|
+
except AuthError:
|
|
103
|
+
print("Check your API key")
|
|
104
|
+
except JobError as e:
|
|
105
|
+
print(f"Job failed: {e}")
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Context manager
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
with OliveClient(api_key="olv_...") as client:
|
|
112
|
+
vectors = client.embeddings(["hello"])
|
|
113
|
+
```
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from .client import (
|
|
2
|
+
OliveClient, AsyncOliveClient,
|
|
3
|
+
Job, AsyncJob,
|
|
4
|
+
OliveError, AuthError, JobError,
|
|
5
|
+
NotFoundError, RateLimitError, ServerError,
|
|
6
|
+
)
|
|
7
|
+
from .compute import App, Image, Function, ComputeError
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"OliveClient", "AsyncOliveClient",
|
|
11
|
+
"Job", "AsyncJob",
|
|
12
|
+
"OliveError", "AuthError", "JobError",
|
|
13
|
+
"NotFoundError", "RateLimitError", "ServerError",
|
|
14
|
+
"App", "Image", "Function", "ComputeError",
|
|
15
|
+
]
|
|
16
|
+
__version__ = "0.1.1"
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""olive CLI — `olive deploy script.py`, `olive call`, `olive list`, etc.
|
|
2
|
+
|
|
3
|
+
Wired up via the [project.scripts] entry point in pyproject.toml.
|
|
4
|
+
|
|
5
|
+
Reads ``OLIVE_API_KEY`` from the environment (or accepts ``--api-key``).
|
|
6
|
+
Reads ``OLIVE_API_URL`` if overriding the default backend.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import importlib.util
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from .client import OliveClient
|
|
20
|
+
from .compute import App
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _load_module(path: str):
|
|
24
|
+
"""Import a Python file by path. Returns the module (with side effects
|
|
25
|
+
from any top-level code, including ``@app.function`` registrations)."""
|
|
26
|
+
p = Path(path).resolve()
|
|
27
|
+
if not p.exists():
|
|
28
|
+
sys.exit(f"error: file not found: {path}")
|
|
29
|
+
spec = importlib.util.spec_from_file_location(p.stem, p)
|
|
30
|
+
if spec is None or spec.loader is None:
|
|
31
|
+
sys.exit(f"error: failed to import {path}")
|
|
32
|
+
mod = importlib.util.module_from_spec(spec)
|
|
33
|
+
spec.loader.exec_module(mod)
|
|
34
|
+
return mod
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _find_apps(mod) -> list[App]:
|
|
38
|
+
"""Find all module-level App instances."""
|
|
39
|
+
return [v for v in vars(mod).values() if isinstance(v, App)]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _client_from_args(args) -> OliveClient:
|
|
43
|
+
api_key = args.api_key or os.environ.get("OLIVE_API_KEY")
|
|
44
|
+
if not api_key:
|
|
45
|
+
sys.exit(
|
|
46
|
+
"error: no API key. Set OLIVE_API_KEY or pass --api-key. "
|
|
47
|
+
"Get one at https://customer.olivecompute.com"
|
|
48
|
+
)
|
|
49
|
+
base = args.api_url or os.environ.get("OLIVE_API_URL") or "https://api.olivecompute.com"
|
|
50
|
+
return OliveClient(api_key=api_key, base_url=base)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ── Commands ──────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def cmd_deploy(args):
|
|
57
|
+
mod = _load_module(args.file)
|
|
58
|
+
apps = _find_apps(mod)
|
|
59
|
+
if not apps:
|
|
60
|
+
sys.exit(
|
|
61
|
+
f"error: no App found in {args.file}. Define one with "
|
|
62
|
+
"`from olive import App; app = App('my-app')`."
|
|
63
|
+
)
|
|
64
|
+
client = _client_from_args(args)
|
|
65
|
+
for app in apps:
|
|
66
|
+
if not app.functions:
|
|
67
|
+
print(f"⚠ app {app.name!r}: no @app.function decorators found")
|
|
68
|
+
continue
|
|
69
|
+
records = app.deploy(client)
|
|
70
|
+
print(f"✓ app {app.name!r}: deployed {len(records)} function(s)")
|
|
71
|
+
for fn, r in zip(app.functions, records):
|
|
72
|
+
print(f" • {fn.remote_name} → {r.get('function_id')}")
|
|
73
|
+
print(f" image: {r.get('image_uri')}")
|
|
74
|
+
print(f" invoke: olive call {fn.remote_name} '<json input>'")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def cmd_list(args):
|
|
78
|
+
client = _client_from_args(args)
|
|
79
|
+
fns = client.list_functions()
|
|
80
|
+
if not fns:
|
|
81
|
+
print("no compute functions deployed")
|
|
82
|
+
return
|
|
83
|
+
print(f"{'NAME':<40} {'TIER':<8} {'TIMEOUT':<8} {'INVOCATIONS':<12} {'VERSION'}")
|
|
84
|
+
for f in fns:
|
|
85
|
+
print(
|
|
86
|
+
f"{f['name']:<40} {f['compute_tier']:<8} {str(f['timeout_seconds'])+'s':<8} "
|
|
87
|
+
f"{str(f['invocation_count']):<12} {f['version']}"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def cmd_call(args):
|
|
92
|
+
client = _client_from_args(args)
|
|
93
|
+
try:
|
|
94
|
+
input_value: Any = json.loads(args.input) if args.input else None
|
|
95
|
+
except json.JSONDecodeError as e:
|
|
96
|
+
sys.exit(f"error: input is not valid JSON: {e}")
|
|
97
|
+
result = client.compute_call(args.name, input_value)
|
|
98
|
+
if isinstance(result, (dict, list)):
|
|
99
|
+
print(json.dumps(result, indent=2))
|
|
100
|
+
else:
|
|
101
|
+
print(result)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def cmd_spawn(args):
|
|
105
|
+
client = _client_from_args(args)
|
|
106
|
+
try:
|
|
107
|
+
input_value: Any = json.loads(args.input) if args.input else None
|
|
108
|
+
except json.JSONDecodeError as e:
|
|
109
|
+
sys.exit(f"error: input is not valid JSON: {e}")
|
|
110
|
+
r = client.compute_spawn(args.name, input_value)
|
|
111
|
+
print(json.dumps(r, indent=2))
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def cmd_delete(args):
|
|
115
|
+
client = _client_from_args(args)
|
|
116
|
+
ok = client.delete_function(args.name)
|
|
117
|
+
if ok:
|
|
118
|
+
print(f"✓ deleted: {args.name}")
|
|
119
|
+
else:
|
|
120
|
+
sys.exit(f"error: function not found: {args.name}")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# ── Top-level parser ──────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
127
|
+
p = argparse.ArgumentParser(prog="olive", description="Olive Compute CLI")
|
|
128
|
+
p.add_argument("--api-key", help="Olive API key (default: $OLIVE_API_KEY)")
|
|
129
|
+
p.add_argument("--api-url", help="Olive API base URL (default: $OLIVE_API_URL or production)")
|
|
130
|
+
|
|
131
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
132
|
+
|
|
133
|
+
d = sub.add_parser("deploy", help="Deploy compute functions from a Python file")
|
|
134
|
+
d.add_argument("file", help="Path to script.py containing an App + @app.function decorators")
|
|
135
|
+
d.set_defaults(func=cmd_deploy)
|
|
136
|
+
|
|
137
|
+
l = sub.add_parser("list", help="List your deployed compute functions")
|
|
138
|
+
l.set_defaults(func=cmd_list)
|
|
139
|
+
|
|
140
|
+
c = sub.add_parser("call", help="Synchronously invoke a function")
|
|
141
|
+
c.add_argument("name", help="Function name (or function_id)")
|
|
142
|
+
c.add_argument("input", nargs="?", default="", help="JSON input value (optional)")
|
|
143
|
+
c.set_defaults(func=cmd_call)
|
|
144
|
+
|
|
145
|
+
s = sub.add_parser("spawn", help="Async invoke — returns job id immediately")
|
|
146
|
+
s.add_argument("name", help="Function name (or function_id)")
|
|
147
|
+
s.add_argument("input", nargs="?", default="", help="JSON input value (optional)")
|
|
148
|
+
s.set_defaults(func=cmd_spawn)
|
|
149
|
+
|
|
150
|
+
rm = sub.add_parser("delete", help="Delete a compute function")
|
|
151
|
+
rm.add_argument("name", help="Function name (or function_id)")
|
|
152
|
+
rm.set_defaults(func=cmd_delete)
|
|
153
|
+
|
|
154
|
+
return p
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def main(argv: list[str] | None = None) -> None:
|
|
158
|
+
args = build_parser().parse_args(argv)
|
|
159
|
+
args.func(args)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
if __name__ == "__main__":
|
|
163
|
+
main()
|