Metadata-Version: 2.4
Name: erpdesktop
Version: 1.0.4
Summary: ERPDesktop plugin SDK — build, package, and publish plugins for the ERPDesktop marketplace
Author-email: BatchNepal <support@batchnepal.com>
License: MIT
Requires-Python: >=3.11
Description-Content-Type: text/markdown
Requires-Dist: click>=8.0
Requires-Dist: httpx>=0.27
Requires-Dist: jsonschema>=4.20
Requires-Dist: rich>=13.0
Requires-Dist: pyyaml>=6.0
Requires-Dist: keyring>=25.0
Requires-Dist: cryptography>=42.0
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
Requires-Dist: ruff>=0.5; extra == "dev"

# ERPDesktop Plugin SDK

Build, package, and publish plugins for the [ERPDesktop](https://erpdesktop.com) marketplace.
ERPDesktop plugins run as Python sidecars alongside the desktop app and communicate with
ERPNext/Odoo over a secure JSON-RPC channel.

## Install

```bash
pip install erpdesktop
```

Requires Python 3.11+.

---

## Quick start

```bash
# Scaffold a new plugin
erpdesktop init com.yourcompany.myplugin

# Validate plugin.json
erpdesktop validate

# Package into a distributable .zip
erpdesktop package

# Publish to the marketplace
erpdesktop publish --changelog "Initial release"
```

---

## Plugin lifecycle

```python
from erpdesktop import PluginBase, log, event

class MyPlugin(PluginBase):
    plugin_id = "com.example.myplugin"
    plugin_version = "1.0.0"

    def on_start(self, config: dict) -> None:
        """Called when the plugin is started. Register your commands here."""
        log("info", "Plugin started", branch=config.get("branch"))
        self.register_command("ping", self.handle_ping)

    def on_stop(self) -> None:
        """Called on graceful shutdown."""
        log("info", "Plugin stopping")

    def on_config_change(self, config: dict) -> None:
        """Called when the user updates plugin settings."""
        pass

    def handle_ping(self, params: dict) -> dict:
        return {"pong": True}

if __name__ == "__main__":
    MyPlugin().run()
```

---

## Modules

### `log` — structured logging

```python
from erpdesktop import log

log("info", "Sync complete", records=47, duration_ms=320)
log("error", "Device unreachable", device_id="K50-001")
# levels: debug | info | warn | error | critical
```

### `event` — telemetry

Emit custom analytics events visible in the Publisher Dashboard.
Do **not** include personal data or ERP record contents.

```python
from erpdesktop import event

event("sync_completed", records=47, duration_ms=320)
event("device_connected", model="K50", firmware="2.1.3")
```

### `erpnext_request` / `odoo_request` — ERP API

Call your connected ERPNext or Odoo site. Requires `network.outbound.erpnext` permission.

```python
from erpdesktop import erpnext_request, odoo_request

# ERPNext
result = erpnext_request("GET", "/api/resource/Employee", params={"limit": 10})

# Odoo
result = odoo_request("POST", "/web/dataset/call_kw", json={...})
```

### `http` — external HTTP

Make calls to third-party APIs. Requires `network.outbound.internet` permission and
the endpoint declared in `network_endpoints` in your `plugin.json`.

```python
from erpdesktop import http

resp = http.get("https://api.example.com/data", params={"key": "val"})
resp = http.post("https://api.example.com/sync", json={"records": [...]})
```

### `storage` — persistent key-value store

```python
from erpdesktop import storage

storage.set("last_sync", "2024-01-15T10:30:00")
value = storage.get("last_sync")          # returns str or None
value = storage.get("last_sync", "never") # with fallback
```

### `secrets` — encrypted credential storage

Store API keys and passwords in the OS keychain — never in config files.

```python
from erpdesktop import secrets

secrets.set("api_key", "sk-abc123")
key = secrets.get("api_key")          # returns str or None
key = secrets.get("api_key", "")      # with fallback
```

### `permissions` — runtime permission checks

```python
from erpdesktop import permissions

# Check before doing something sensitive
if permissions.require("network.outbound.internet"):
    result = http.get("https://api.example.com/data")

# Raise PermissionDenied automatically if not granted
permissions.enforce("network.outbound.erpnext")

# Check multiple
if permissions.has_all("network.outbound.erpnext", "storage.read"):
    ...

if permissions.has_any("network.outbound.erpnext", "network.outbound.internet"):
    ...
```

### `device` — biometric/access device leasing

Claim exclusive access to a physical device (e.g. fingerprint reader) so two plugins
don't conflict.

```python
from erpdesktop import device

if device.lease("ZK-192.168.1.10", metadata={"model": "K50"}):
    # we have exclusive access
    ...
    device.release("ZK-192.168.1.10")

device.renew("ZK-192.168.1.10")            # extend lease
owner = device.is_leased("ZK-192.168.1.10") # returns plugin_id or None
```

### `notify` — desktop notifications

```python
from erpdesktop import notify

notify.send("Sync complete", "47 attendance records pushed to ERPNext")
notify.send("Device offline", "ZK-K50 not reachable", urgent=True)
```

### `i18n` — translations

```python
from erpdesktop import i18n

i18n.set_locale("ne")                              # Nepali
label = i18n.t("sync.button", fallback="Sync")
label = i18n.t("greeting", fallback="Hello {name}", name="Ram")
```

Place translation files at `locales/<lang>.json` in your plugin directory:

```json
{ "sync.button": "सिंक गर्नुहोस्", "greeting": "नमस्ते {name}" }
```

---

## `plugin.json` reference

```json
{
  "id": "com.yourcompany.myplugin",
  "name": "My Plugin",
  "version": "1.0.0",
  "description": "What your plugin does.",
  "author": "Your Company",
  "plugin_type": "commercial",
  "entry": "main.py",
  "permissions": [
    "network.outbound.erpnext",
    "network.outbound.internet",
    "storage.read",
    "storage.write"
  ],
  "network_endpoints": [
    {
      "url": "https://api.example.com",
      "purpose": "Sync attendance records",
      "data_sent": "Employee IDs, timestamps"
    }
  ],
  "config_schema": {
    "branch": { "type": "string", "label": "Branch", "required": true }
  }
}
```

**`plugin_type`** — `open_source` (requires `source_url`), `commercial`, or `private`

**`network_endpoints`** — required when `network.outbound.internet` is declared.
Shown to the user at install time. The SDK enforces the allowlist at runtime.

---

## CLI reference

| Command | Description |
| --- | --- |
| `erpdesktop login` | Authenticate with the ERPDesktop marketplace |
| `erpdesktop logout` | Sign out |
| `erpdesktop whoami` | Show current logged-in publisher |
| `erpdesktop init <id>` | Scaffold a new plugin |
| `erpdesktop validate` | Validate `plugin.json` against the schema |
| `erpdesktop package` | Build a `.zip` for distribution |
| `erpdesktop publish` | Publish to the marketplace |
| `erpdesktop logs` | Stream live plugin logs |
| `erpdesktop dev` | Start plugin in dev/watch mode |
| `erpdesktop --version` | Show SDK version |

---

## Publishing checklist

1. `plugin_type` is set correctly
2. All `permissions` used in code are declared in `plugin.json`
3. Every external URL in `http.*` calls is listed in `network_endpoints`
4. No ERP record data passed to `event()`
5. `erpdesktop validate` passes with no errors
6. `erpdesktop package` produces a valid `.zip`
7. `erpdesktop publish --changelog "What changed"`

---

## License

MIT © [BatchNepal](https://batchnepal.com)
