Metadata-Version: 2.4
Name: makers-sdk
Version: 0.1.0b2
Summary: Synchronous Python client for the EdgeOne Makers SDK
License-Expression: MIT
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: cos-python-sdk-v5==1.9.44
Provides-Extra: test
Requires-Dist: jsonschema[format]>=4.18; extra == "test"

# EdgeOne Makers Python SDK

[English](README.md) | [中文](README.zh-CN.md)

Synchronous Python 3.10+ client for EdgeOne Makers projects, environment variables, and artifact deployments. There is no async Makers.

PyPI package: `makers-sdk` · `import makers_sdk` · Version `0.1.0` · Contract `0.1.36` · MIT License

## Installation

```sh
pip install makers-sdk
```

```python
from makers_sdk import Makers
```

---

## Makers

### Constructor

```python
import os
from makers_sdk import Makers

makers = Makers(
    token=os.environ["MAKERS_API_TOKEN"],
    region="china",
)
```

| Parameter | Type | Required | Default | Description |
|-----------|------|:--------:|---------|-------------|
| `token` | `str` | Yes | — | EdgeOne Makers API Token |
| `source` | `str` | No | `sdk` | Default `"sdk"`. An explicit value such as `"cli"` is forwarded unchanged. |
| `region` | `str` | No | auto-detect | `"china"` → `pages-api.cloud.tencent.com/v1`; `"global"` → `pages-api.edgeone.ai/v1` |
| `base_url` | `str` | No | — | Overrides `region`; must be HTTPS (localhost may use HTTP) |
| `timeout` | `float` | No | `30` | Per-request timeout in **seconds** |
| `retries` | `int` | No | `3` | Max retry count for queries; writes do not retry |
| `logger` | `Logger` | No | — | Must implement `debug`, `info`, `warn`, `error` |

Auto-detection probes china then global and caches the result per Makers instance.

### Public members

| Member | Type | Description |
|--------|------|-------------|
| `makers.projects` | `Projects` | Project and environment variable operations |
| `makers.deployments` | `Deployments` | Deployment operations |
| `makers.region` | `str \| None` | Read-only; configured or detected region |

---

## Projects

All return values are `dict` with `snake_case` keys. Optional fields may be absent — always use `.get()`.

### `projects.create(*, name, area?, initial_env_vars?)`

Creates a project. Returns `{"project_id": ...}` only — query `get` for the full model.

```python
created = makers.projects.create(
    name="docs-site",
    area="overseas",
    initial_env_vars=[{"key": "API_URL", "value": "https://example.com"}],
)
project_id = created["project_id"]
```

| Parameter | Type | Required | Description |
|-----------|------|:--------:|-------------|
| `name` | `str` | Yes | Unique per account; duplicate raises `ConflictError` |
| `area` | `str` | No | `"mainland"` / `"overseas"` / `"global"`; omitted = not sent |
| `initial_env_vars` | `list[dict]` | No | Set env vars at creation time |

### `projects.list(**options)`

```python
page = makers.projects.list(
    name="docs",
    page=0,
    page_size=20,
    order={"field": "created_on", "direction": "desc"},
)
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `project_ids` | `list[str]` | — | Filter by ID list |
| `name` | `str` | — | Filter by name |
| `status` | `str` | — | Filter by status |
| `provider` | `str` | — | Filter by provider |
| `page` | `int` | `0` | Zero-based page index |
| `page_size` | `int` | `20` | 1–100 |
| `order` | `dict` | — | `field`: `"created_on"` or `"modified_on"`; `direction`: `"asc"` or `"desc"` |

**Returns** `dict`: `{ "items", "page", "page_size", "total", "has_next" }`.

### `projects.list_all(**options)`

```python
for project in makers.projects.list_all():
    print(project["project_id"], project["name"])
```

Same options as `list` except no `page`. Returns `Iterator[dict]`.

### `projects.get(*, project_id)`

Returns the full project dict. This is the only way to obtain `preset_domain`.

```python
project = makers.projects.get(project_id=project_id)
print(project.get("preset_domain"))
```

### `projects.update(*, project_id, **fields)`

Updates project settings. Only provided fields are sent; omitted fields are not cleared.

```python
makers.projects.update(
    project_id=project_id,
    name="new-name",
    root_dir=".",
    output_dir="dist",
    build_cmd="npm run build",
    install_cmd="npm install",
    framework="other",
    nodejs_version="20",
)
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `project_id` | `str` | Required |
| `name` | `str` | Project name |
| `root_dir` | `str` | Root directory |
| `output_dir` | `str` | Output directory |
| `build_cmd` | `str` | Build command |
| `install_cmd` | `str` | Install command |
| `framework` | `str` | Framework identifier |
| `nodejs_version` | `str` | Node.js version |

### `projects.delete(*, project_id)`

Deletes a project. **Irreversible.**

---

## Environment variables

Methods live on `makers.projects`. Public fields: `key`, `value`, optional `comment`. Change project variables with `set_envs`; `deploy` does not accept env vars.

### `projects.list_envs(*, project_id)`

Returns `list[dict]`. Values are automatically added to the redaction list.

### `projects.set_envs(*, project_id, env_vars)`

Batch upsert. Internally reads existing vars first, then writes. Not atomic. Duplicate keys raise `ValidationError` before any network request.

```python
makers.projects.set_envs(
    project_id=project_id,
    env_vars=[
        {"key": "API_URL", "value": "https://example.com", "comment": "origin"},
    ],
)
```

### `projects.delete_envs(*, project_id, keys)`

Deletes env vars by key. Duplicate keys raise `ValidationError`.

---

## Deployments

### `deployments.deploy(*, project_id, artifact, **options)`

Deploys an artifact. Accepts exactly one variant:

| Variant | Type | Behavior |
|---------|------|----------|
| `"files"` | `dict[str, str \| bytes]` | SDK zips in memory |
| `"directory"` | `str` (+ optional `"exclude_patterns": list[str]`) | SDK zips the directory |
| `"archive"` | `str` (zip file path) | Uploaded directly after safety validation |

```python
# Inline files
makers.deployments.deploy(
    project_id=project_id,
    artifact={"files": {"index.html": "<h1>ok</h1>"}},
    wait=True,
)

# Application source (project root)
makers.deployments.deploy(
    project_id=project_id,
    artifact={"directory": "."},
    wait=True,
)

# Static build output
makers.deployments.deploy(
    project_id=project_id,
    artifact={"directory": "./dist", "exclude_patterns": ["**/*.map"]},
)

# Prebuilt CLI output
makers.deployments.deploy(
    project_id=project_id,
    artifact={"directory": "./.edgeone"},
    wait=True,
)

# Zip archive
makers.deployments.deploy(
    project_id=project_id,
    artifact={"archive": "./site.zip"},
)
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `project_id` | `str` | — | Required |
| `artifact` | `dict` | — | Required; exactly one variant |
| `env` | `str` | `"Production"` | `"Preview"` requires an existing production deployment |
| `wait` | `bool` | `False` | Poll until terminal status |
| `timeout` | `float` | `900` | Max wait time in **seconds** (15 min) |
| `poll_interval` | `float` | `5` | Poll interval in **seconds** |
| `upload_progress` | callable | — | Fires once after upload completes |
| `status_change` | callable | — | Fires on each status transition |

**Returns** `dict`. With `wait=False`, only `deployment_id`, `project_id`, and `env` are populated — that is not an openable site URL. After `wait=True` reaches `Success`, `preview_url` is an openable link and **expires**.

**Note**: `timeout` here is the overall wait budget (default 900s), distinct from the per-request `Makers(timeout=30)`.

**Directory input**: pass one directory. Typical inputs are application source (project root), static build output (`index.html` must be at that directory root), or prebuilt Makers/SSR output (`./.edgeone` after a local CLI build). Application source is the usual SDK path: the SDK zips the tree with default ignores and uploads it. It does not compile locally and does not itself run `npm run build` or `edgeone makers build`. For Upload projects, Pages may then run `edgeone makers build`. The SDK does not run framework adapters. Default ignores are a fixed list; the SDK does not read `.gitignore`. A caller-provided `archive` is uploaded as-is.

**Directory safety**: rejects symlinks; ignores `.git`, `node_modules`, the artifact-root `.edgeone` directory, `.env`, logs, temp and system files. Non-`.edgeone` directories also ignore any path segment that starts with `.`; `.well-known` is kept. Nested `.edgeone` paths are kept. Passing a directory named `.edgeone` packs CLI layout (`<parent>/.edgeone/...`, plus sibling `edgeone.json` when present), keeps nested `node_modules`, and keeps hidden files. Other directories still drop `node_modules` at any depth. These defaults are a fixed list, not `.gitignore`. `exclude_patterns` appends POSIX globs; negation (`!`) is rejected.

### `deployments.wait(*, project_id, deployment_id, **options)`

Polls until a terminal status: `Success`, `Failed`, `Timeout`, `Cancelled`, or `Invalid`. After `Success`, `preview_url` is an openable site URL (signed for default upload projects) and **expires**. `get` / `list` / `wait=False` do not sign.

```python
result = makers.deployments.wait(
    project_id=project_id,
    deployment_id=deployment_id,
    timeout=900,
    poll_interval=5,
    status_change=lambda e: print(e["deployment"].get("status")),
)
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `project_id` | `str` | — | Required |
| `deployment_id` | `str` | — | Required |
| `timeout` | `float` | `900` | Raises `DeploymentTimeoutError` on expiry; **does not cancel the deployment** |
| `poll_interval` | `float` | `5` | |
| `status_change` | callable | — | |

### `deployments.list(*, project_id, **options)`

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `project_id` | `str` | — | Required |
| `status` | `list[str]` | — | Filter by status |
| `time_range` | `dict` | — | `{"start": ..., "end": ...}` ISO 8601 |
| `repo_branch` | `list[str]` | — | Filter by branch |
| `page` | `int` | `0` | |
| `page_size` | `int` | `20` | |
| `order` | `dict` | — | |

### `deployments.list_all(*, project_id, **options)`

Same as `list` without `page`. Returns `Iterator[dict]`.

### `deployments.get(*, project_id, deployment_id)`

Returns a single deployment dict. Raises `NotFoundError` if not found.

### `deployments.get_log(*, project_id, deployment_id)`

Returns `{"log_url": ...}` — the build log URL.

---

## Data models

All values are plain `dict`. Optional fields may be absent — use `.get()` to access them.

### Project

| Field | Type | Always present | Description |
|-------|------|:--------------:|-------------|
| `project_id` | `str` | Yes | |
| `name` | `str` | Yes | Unique per account |
| `status` | `str` | Yes | |
| `area` | `str` | No | `"mainland"` / `"overseas"` / `"global"` |
| `preset_domain` | `str` | No | Production domain; may be absent for new projects |
| `created_on` | `str` | Yes | ISO 8601 |
| `modified_on` | `str` | Yes | ISO 8601 |

### Deployment

| Field | Type | Always present | Description |
|-------|------|:--------------:|-------------|
| `deployment_id` | `str` | Yes | |
| `project_id` | `str` | Yes | |
| `env` | `str` | Yes | `"Production"` or `"Preview"` |
| `status` | `str` | No | Terminal: `Success` / `Failed` / `Timeout` / `Cancelled` / `Invalid` |
| `preview_url` | `str` | No | After wait Success: openable site URL (expires). `get` / `wait=False` may expose the raw backend URL, which is not openable. |
| `code` | `str` | No | |
| `created_on` | `str` | No | Not returned with `wait=False` |
| `modified_on` | `str` | No | Not returned with `wait=False` |

### EnvVar

| Field | Type | Always present |
|-------|------|:--------------:|
| `key` | `str` | Yes |
| `value` | `str` | Yes |
| `comment` | `str` | No |

### Callbacks

**upload_progress event**: `{"uploaded_bytes", "total_bytes", "completed_files", "total_files"}`

**status_change event**: `{"deployment": dict, "previous_status": str | None}`

---

## Errors

All errors extend `MakersError` with fields: `code`, `message`, `request_id`, `http_status`, `cause`.

| Error class | Trigger |
|-------------|---------|
| `AuthError` | Code 105 or HTTP 401 |
| `ValidationError` | Code contains "Invalid" or HTTP 400 |
| `NotFoundError` | Code contains "NotFound" or HTTP 404 |
| `ConflictError` | Code contains "Conflict" or HTTP 409 |
| `RateLimitError` | Code 110 or HTTP 429 |
| `UploadError` | COS upload failure |
| `TimeoutError` | Request timeout |
| `DeploymentTimeoutError` | Wait timeout (extends `TimeoutError`) |

```python
from makers_sdk import MakersError, NotFoundError, DeploymentTimeoutError

try:
    makers.projects.get(project_id="missing")
except NotFoundError as error:
    print(error.code, error.request_id, error.http_status)
except MakersError:
    raise
```

### Retry behavior

Queries retry on network errors, HTTP 408/429/5xx, and outer `Code: 110`, up to `retries` times (default 3). Exponential backoff with full jitter, capped at 10 seconds. `Retry-After` header is respected when present. Creates, updates, and COS credential requests do not retry.

---

## Development

```sh
python -m pip install -e ".[test]"
python -m unittest discover -s tests -v
```
