Metadata-Version: 2.4
Name: remotepy
Version: 0.2.0
Summary: remotepy allows Python functions to be called remotely from multiple languages including JavaScript, CSharp and Python
Author-email: Faraz Farukh Tamboli <faraz.tamboli@gmail.com>
License: Proprietary
Project-URL: Homepage, https://www.remotepy.com
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: License :: Other/Proprietary License
Classifier: Operating System :: OS Independent
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: autobahn[asyncio,encryption,serialization,serializers]
Requires-Dist: bcrypt
Requires-Dist: msgpack
Requires-Dist: numpy
Requires-Dist: python-dotenv
Requires-Dist: pyzmq
Requires-Dist: shortuuid
Provides-Extra: mysql
Requires-Dist: mysqleasy; extra == "mysql"
Requires-Dist: mysql-connector-python; extra == "mysql"
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Requires-Dist: pytest-asyncio; extra == "dev"
Requires-Dist: websockets; extra == "dev"
Provides-Extra: all
Requires-Dist: mysqleasy; extra == "all"
Requires-Dist: mysql-connector-python; extra == "all"
Dynamic: license-file

# remotepy

remotepy is a powerful RPC (Remote Procedure Call) framework that lets you write Python
functions and call them remotely from JavaScript, C#, or Python over WebSockets.

---

## Table of contents

1. [Features](#features)
2. [Installation](#installation)
3. [Quick-start (5 minutes, no database)](#quick-start-no-database)
4. [Automated setup wizard](#automated-setup-wizard)
5. [Session management — choosing a backend](#session-management)
6. [MySQL setup guide](#mysql-setup-guide)
7. [Advanced features](#advanced-features)
8. [Client examples](#client-examples)
9. [Configuration reference](#configuration-reference)
10. [Security features](#security-features)
11. [API reference](#api-reference)
12. [Best practices](#best-practices)
13. [Troubleshooting](#troubleshooting)

---

## Features

- **Multi-language support** — call Python from JavaScript (browser), C#, or Python
- **Decorator API** — expose functions with `@remotepy_func` / `@remotepy_class`
- **Session management** — built-in, works **with or without MySQL**
- **In-memory sessions** — zero-dependency mode, no database needed
- **MySQL sessions** — persistent sessions backed by MySQL
- **Pluggable storage** — bring your own session backend (Redis, SQLite, etc.)
- **Async / streaming** — `async def`, generators, async generators
- **CPU-bound work** — `@cpu_bound` runs heavy tasks in a `ProcessPoolExecutor`
- **SSL/TLS** — `run_ssl()` for secure `wss://` connections
- **Pub/Sub** — `PubSubBroadcastServerFactory` for real-time broadcasting
- **Security** — rate limiting, input validation, password sanitization in logs
- **Setup wizard** — `remotepy-setup` generates a working server in seconds

---

## Installation

### Without MySQL (in-memory sessions)

```bash
pip install remotepy
```

### With MySQL support

```bash
pip install remotepy[mysql]
```

---

## Quick-start — no database

You can run a full session-managed server with zero external dependencies:

```python
# server.py
from remotepy import *
from remotepy.session.SimpleSessionServer import SimpleSessionServer


@remotepy_class
class MyServer(WebSocketRPCServerProtocol, SimpleSessionServer):

    def __init__(self):
        super().__init__()
        # Seed users programmatically — or call self.load_users_from_env()
        self.add_user("alice", "password123", "alice@example.com")
        self.add_user("bob",   "s3cret",      "bob@example.com")

    def onConnect(self, request):
        print("Client connecting:", request.peer)

    @remotepy_method
    def hello(self, name):
        """Say hello."""
        return f"Hello, {name}!"

    def onClose(self, wasClean, code, reason):
        print("Client closed:", code, reason)


if __name__ == "__main__":
    import logging
    logging.basicConfig(level=logging.INFO)
    server = MyServer()
    server.run("0.0.0.0", 8082)
```

Run it:

```bash
python server.py
```

---

## Automated setup wizard

The fastest way to get started:

```bash
pip install remotepy
remotepy-setup
```

The wizard:
1. Asks whether you want **in-memory** or **MySQL** session storage
2. Generates a ready-to-run `server.py`
3. Generates an `.env.example` with all settings documented
4. (MySQL only) generates `schema.sql` to create all required tables
5. (MySQL only) optionally generates `docker-compose.yml` for local dev MySQL

---

## Session management

remotepy provides three ways to handle sessions.  Pick what fits your project:

### Option 1 — In-memory, no database (simplest)

Use `SimpleSessionServer`.  Zero setup, zero dependencies beyond `remotepy` itself.
Users and sessions are stored in RAM — state is lost on restart.

**Best for:** development, prototyping, small apps, microservices.

```python
from remotepy import *
from remotepy.session.SimpleSessionServer import SimpleSessionServer

@remotepy_class
class MyServer(WebSocketRPCServerProtocol, SimpleSessionServer):
    def __init__(self):
        super().__init__()
        self.add_user("admin", "secret", "admin@example.com")
```

Load users from an environment variable instead of hardcoding them:

```bash
export REMOTEPY_USERS="admin:secret:admin@example.com,guest:guest:guest@example.com"
```

```python
def __init__(self):
    super().__init__()
    self.load_users_from_env()
```

### Option 2 — Custom auth, in-memory sessions (flexible)

Inherit from `SessionServer` with `db_config=None` and override `_authenticate_user`:

```python
from remotepy import *

@remotepy_class
class MyServer(WebSocketRPCServerProtocol, SessionServer):
    def __init__(self):
        super().__init__(None)   # None → InMemorySessionStorage

    def _authenticate_user(self, username: str, password: str) -> bool:
        # Your own auth logic — check a file, API, LDAP, etc.
        return username == "admin" and password == "secret"

    @remotepy_method
    def hello(self, name):
        return f"Hello, {name}!"
```

### Option 3 — MySQL (production)

Full persistent sessions + user management:

```python
from remotepy import *

@remotepy_class
class MyServer(WebSocketRPCServerProtocol, SessionServer):
    def __init__(self):
        super().__init__(db_config={
            "host":     "localhost",
            "user":     "remotepy",
            "password": "changeme",
            "database": "myschema",
        }, use_schema_name="myschema")
        self.set_email_config()  # reads REMOTEPY_EMAIL_* env vars
```

### Option 4 — Custom storage backend

Implement `SessionStorageBackend` and pass it directly:

```python
from remotepy import *
from remotepy.session.storage.base import SessionStorageBackend

class RedisSessionStorage(SessionStorageBackend):
    # ... implement all abstract methods ...

@remotepy_class
class MyServer(WebSocketRPCServerProtocol, SessionServer):
    def __init__(self):
        super().__init__(storage_backend=RedisSessionStorage("redis://localhost"))
```

---

## MySQL setup guide

### Step 1 — Install with MySQL extra

```bash
pip install remotepy[mysql]
```

### Step 2 — Create the schema

Use the automated wizard (recommended):

```bash
remotepy-setup
# Choose option 2 (MySQL)
# This generates schema.sql automatically
```

Or run the schema manually:

```bash
mysql -u root -p < schema.sql
```

The schema creates these tables in your chosen database:

| Table | Purpose |
|---|---|
| `session_state` | One row per active WebSocket connection |
| `session_variable_blob` | Key-value store for session variables |
| `new_session_ids` | One-time session tokens |
| `user` | User accounts (username, bcrypt password, email, address) |
| `address` | Postal addresses linked to users |

### Step 3 — Local dev with Docker

The setup wizard generates a `docker-compose.yml`.  Start MySQL with:

```bash
docker compose up -d
```

Credentials default to: user `remotepy`, password `changeme`, port `3306`.

### Step 4 — Configure via .env

Copy `.env.example` to `.env` and fill in your values:

```ini
DB_HOST=localhost
DB_USER=remotepy
DB_PASSWORD=changeme
DB_NAME=myschema

REMOTEPY_EMAIL_USER=your@email.com
REMOTEPY_EMAIL_PASSWORD=app-password
REMOTEPY_EMAIL_SERVER=smtp.gmail.com
REMOTEPY_EMAIL_PORT=587
REMOTEPY_RESET_PASSWORD_EMAIL=noreply@yourdomain.com
REMOTEPY_DOMAIN_NAME=https://www.yourdomain.com
```

---

## Advanced features

### Async functions

```python
@remotepy_class
class AsyncServer(WebSocketRPCServerProtocol):

    @remotepy_func
    async def fetch_data(self, url):
        import aiohttp
        async with aiohttp.ClientSession() as s:
            async with s.get(url) as r:
                return await r.json()
```

### Streaming with generators

```python
@remotepy_func
def stream_rows(self, count):
    for i in range(count):
        yield {"index": i, "data": f"item_{i}"}

@remotepy_func
async def stream_async(self, count):
    for i in range(count):
        await asyncio.sleep(0.05)
        yield {"index": i, "data": f"item_{i}"}
```

### CPU-bound work

```python
from remotepy import *
import numpy as np

@remotepy_class
class MLServer(WebSocketRPCServerProtocol):

    @remotepy_method
    @cpu_bound              # runs in ProcessPoolExecutor — bypasses the GIL
    def run_inference(self, data):
        arr = np.array(data)
        return heavy_model(arr).tolist()

    @remotepy_method        # runs in ThreadPoolExecutor (default)
    def get_status(self):
        return {"status": "ok"}
```

> **Decorator order matters**: `@cpu_bound` must be *below* `@remotepy_method` /
> `@remotepy_func`.

| Your function does… | Use |
|---|---|
| Database queries, HTTP calls, file I/O | `async def` with `await` |
| Lightweight sync logic | `def` (thread pool) |
| numpy, pandas, ML inference, image processing | `def` + `@cpu_bound` |
| LLM streaming | `async def` with `yield` |

### SSL/TLS

```python
server = MyServer()
server.run_ssl(
    "0.0.0.0", 8443,
    "/etc/ssl/private/server.key",
    "/etc/ssl/certs/fullchain.pem",
)
```

### Protected functions

```python
from remotepy.websocket.remotepy import remotepy_login_required, remotepy_permitted_to

@remotepy_class
class ProtectedServer(WebSocketRPCServerProtocol, SessionServer):

    @remotepy_func
    @remotepy_login_required
    def sensitive_data(self):
        return {"secret": "data"}

    @remotepy_func
    @remotepy_permitted_to("admin")
    def admin_action(self):
        return {"ok": True}
```

### Pub/Sub broadcasting

```python
from remotepy import PubSubBroadcastServerFactory

server = MyServer()
server.run("0.0.0.0", 8082, ServerFactory=PubSubBroadcastServerFactory)
```

---

## Client examples

### JavaScript

```html
<script src="js/remotepy.1.0.0.min.js"></script>
<script>
    var RemotePy = new RemotePyClient();

    window.onload = function() {
        RemotePy.serverName = 'ws://localhost:8082';
        RemotePy.start();
    };

    RemotePy.onopen = function() {
        RemotePy.MyServer.hello("World", function(result) {
            console.log(result);   // "Hello, World!"
        });
    };
</script>
```

### Python

```bash
pip install remotepy_client
```

```python
from remotepy_client.remotepy_rpc_client import RemotePyRPCClientSync

client = RemotePyRPCClientSync("ws://localhost:8082", verbose=False)
client.buildService('')
MyServer = client.getService('MyServer')("ws://localhost:8082", False)

result = MyServer.hello(name="Python", callback=None)
print(result)   # "Hello, Python!"

client.thread().join()
```

---

## Configuration reference

### Environment variables

| Variable | Default | Description |
|---|---|---|
| `REMOTEPY_EMAIL_USER` | — | SMTP user for password-reset emails |
| `REMOTEPY_EMAIL_PASSWORD` | — | SMTP password |
| `REMOTEPY_EMAIL_SERVER` | `smtp-relay.sendinblue.com` | SMTP server |
| `REMOTEPY_EMAIL_PORT` | `587` | SMTP port |
| `REMOTEPY_RESET_PASSWORD_EMAIL` | `info@kubloy.com` | From address for reset emails |
| `REMOTEPY_DOMAIN_NAME` | `http://www.alpharithmic.com` | Base URL for reset links |
| `REMOTEPY_USERS` | — | `user:pass:email,...` for `SimpleSessionServer` |

### Rate limiting

Default: 100 calls per function per 60 seconds.  Customise:

```python
server._rate_limit_window = 60     # seconds
server._rate_limit_max_calls = 200
```

---

## Security features

- **Rate limiting** — per-function call-rate cap (default 100/min)
- **Input validation** — function names and args are validated before dispatch
- **Password sanitization** — passwords are redacted from all log messages and errors
- **Bcrypt hashing** — all user passwords are stored as bcrypt hashes
- **Single-use session tokens** — `new_session_ids` tokens are consumed on first use
- **SQL injection protection** — schema names are sanitized; all queries use parameterized args

---

## API reference

### Decorators

| Decorator | Description |
|---|---|
| `@remotepy_class` | Makes a class a remotepy server |
| `@remotepy_func` | Exposes a function for remote calls |
| `@remotepy_method` | Same as `@remotepy_func`, used with `SessionServer` |
| `@cpu_bound` | Runs in `ProcessPoolExecutor` (stacks below `@remotepy_method`) |
| `@remotepy_login_required` | Requires authenticated session |
| `@remotepy_permitted_to(action)` | Requires specific permission |

### Server methods

| Method | Description |
|---|---|
| `server.run(ip, port)` | Start plain WebSocket server |
| `server.run_ssl(ip, port, key, cert)` | Start TLS WebSocket server |

### SessionServer RPC methods (all storage backends)

| Method | Description |
|---|---|
| `getNewSessionId()` | Generate a one-time session token |
| `startSessionIfNotStarted(sessionid)` | Start session tracking |
| `isLoggedIn(sessionid)` | Check auth status |
| `validateLogin(sessionid, username, password, remember, currentUrl, afterLoginUrl)` | Authenticate |
| `logOut()` | De-authenticate current session |
| `getSessionId()` | Return current session ID |

### SessionServer RPC methods (MySQL or SimpleSessionServer)

| Method | Description |
|---|---|
| `registerLogin(sessionid, username, password, first, middle, last, email, street, city, country)` | Register new user |
| `registerLoginShort(sessionid, username, password, email)` | Register (email + username only) |
| `checkIfUsernameExists(username)` | Check if username/email is taken |
| `forgotPassword(sessionid, email)` | Send password-reset email |
| `resetPassword(sessionid, code, new_password, repeat_password)` | Apply reset |
| `getUserProfile(sessionid)` | Get logged-in user's profile |
| `updateUserProfile(sessionid, ...)` | Update profile |

### SimpleSessionServer extra methods (non-RPC)

| Method | Description |
|---|---|
| `add_user(username, plain_password, email, ...)` | Add a user before `run()` |
| `load_users_from_env()` | Load users from `REMOTEPY_USERS` env var |

### Storage backends

| Class | Import | Notes |
|---|---|---|
| `InMemorySessionStorage` | `remotepy.session.storage.memory` | No dependencies, RAM-only |
| `MySQLSessionStorage` | `remotepy.session.storage.mysql` | Requires `remotepy[mysql]` |
| `SessionStorageBackend` (ABC) | `remotepy.session.storage.base` | Base for custom backends |

---

## Best practices

1. **Choose the right session backend early** — in-memory for dev, MySQL for prod
2. **Use `SimpleSessionServer`** for apps that don't need a database at all
3. **Use SSL in production** — always `run_ssl()` in production environments
4. **Store config in `.env`** — never hardcode passwords in source code
5. **Use `async def` for I/O** and `def + @cpu_bound` for heavy computation
6. **Adjust rate limits** to match your workload
7. **Override `_authenticate_user`** to integrate with existing auth systems

---

## Troubleshooting

### "MySQLSessionStorage requires the 'mysqleasy' package"

Install the MySQL extra:

```bash
pip install remotepy[mysql]
```

### "registerLogin requires MySQL storage"

You are calling a user-management method on a server with in-memory session
storage.  Either:

- Switch to `SimpleSessionServer` for full in-memory user management, **or**
- Pass a `db_config` to use MySQL storage

### Rate limit errors

```python
server._rate_limit_max_calls = 500   # increase cap
```

### Connection issues

- Confirm the server is running: `python server.py`
- Check firewall rules for the port
- WebSocket URL format: `ws://host:port` (plain) or `wss://host:port` (TLS)

---

## Documentation

For more information, visit [https://www.remotepy.com](https://www.remotepy.com)

## License

Proprietary software.  All rights reserved.  See the LICENSE file for details.

## Author

**Faraz Farukh Tamboli** — faraz.tamboli@gmail.com
