ray-python 1.0.0
Python client for the Ray debugging app (port of spatie/ray PHP library)
Requires Python: >=3.13

# Python client for Ray

Python client library for the [Ray](https://myray.app) desktop debugging tool.

Use it to send rich debug information (logs, tables, traces, exceptions, etc.) from Python into Ray with a single `ray(...)` call, mirroring the behavior of the original PHP client.

> Repository: <https://github.com/axro-gmbh/ray>

## Installation

### From PyPI (recommended)

Once the package is published, install it in your project with:

```bash
pip install ray-python
```

Then in your code:

```python
from python_ray import ray

ray("Hello from Python Ray")
```

### From a local checkout (development)

From the root of this repository:

```bash
pip install -e .[dev]
```

This installs the `python_ray` package in editable mode so changes in this repo are immediately reflected.

## Requirements

- Ray desktop app installed and running on your machine (default host/port: `localhost:23517`).
- Python **3.13+** (see `requires-python` in `pyproject.toml`).

## Versioning & compatibility

- This library targets modern CPython and is currently tested with Python 3.13.
- The `requires-python` metadata is set to `>=3.13`; earlier Python versions are not supported.

## Configuration (`ray.json`)

The Python client looks for a `ray.json` file starting from the current working directory and walking up parent directories (similar to how the PHP client searches for `ray.php`).

Example `ray.json`:

```json
{
  "enable": true,
  "host": "localhost",
  "port": 23517,
  "remote_path": null,
  "local_path": null,
  "always_send_raw_values": false
}
```

Available keys:

- `enable` (bool): if `false`, nothing is sent to Ray.
- `host` (str): Ray server host (defaults to `localhost`).
- `port` (int): Ray server port (defaults to `23517`).
- `remote_path` (str|null): remote path prefix to be rewritten in file paths (for origins, traces, etc.).
- `local_path` (str|null): local path prefix that replaces `remote_path` when sending paths.
- `always_send_raw_values` (bool): if true, `ray(...)` always sends values as raw log payloads (no type-based conversion).

If no `ray.json` is found, sensible defaults are used.

## Quick start

Basic usage:

```python
from python_ray import ray

# Simple values
ray("hello")
ray({"foo": "bar"}, 123)

# Chain on the Ray instance
ray("starting").green().label("Init")
```

## Core features

Below are some commonly used features. All examples assume:

```python
from python_ray import ray
```

### Screens and layout

```python
ray().new_screen("Job 1")
ray().clear_all()        # clear everything
ray().clear_screen()     # alias for new_screen("")
ray().separator()        # horizontal separator
```

### Colors, sizes, labels

```python
ray("success").green()
ray("warning").orange()
ray("error").red()

ray("big").large()
ray("small").small()

ray("labeled").label("My label")
```

### Files, images, XML, HTML, URLs

```python
ray().file("README.md")
ray().image("screenshot.png")
ray().xml("<root><item>42</item></root>")
ray().html("<strong>Bold</strong>")
ray().url("https://example.com", "Example")
```

### JSON helpers

```python
# send values encoded with json.dumps
ray().to_json({"a": 1}, [1, 2, 3])

# send decoded JSON strings
ray().json('{"a": 1}', '[1, 2, 3]')
```

### Python runtime info

```python
# high-level summary
ray().pythoninfo()

# specific properties
ray().pythoninfo("version", "implementation")
```

### Traces and caller

```python
ray().trace()              # full-ish Python stack trace (user frames)
ray().trace(limit=10)      # first 10 frames
ray().backtrace()          # alias for trace()
ray().caller()             # single caller frame
```

### Timers (measure / stop_time)

```python
ray().measure("block")
# ... some code ...
ray().measure("block")    # sends total + since-last-call
ray().stop_time("block")  # clear this timer
ray().stop_time()          # clear all timers
```

### Counters

```python
ray().count()               # per-call-site counter
ray().count("my-counter")   # named counter
value = ray().counter_value("my-counter")
ray().clear_counters()
```

### Flow control and rate limiting

```python
r = ray("maybe")
if some_flag:
    r.enable()
else:
    r.disable()

ray().limit(3).send("only three times from here")
ray().once("only once from here")

ray("waiting").pause()  # creates a lock in Ray; resumes when released there
```

### Exceptions and `catch` / `throw_exceptions`

You can pass callables into `send()` that accept a `Ray` instance; any exceptions they raise are captured instead of crashing immediately.

```python
from python_ray import ray

def might_fail(r):
    1 / 0

ray().send(might_fail)   # exception is stored in Ray._caught_exceptions

# Show in Ray:
ray().catch()            # uses Ray.exception() under the hood

# Filter by type:
ray().catch(ZeroDivisionError)

# Or rethrow:
ray().throw_exceptions()
```

You can also directly send exceptions:

```python
try:
    expensive_call()
except Exception as exc:
    ray().exception(exc)
```

### Conditionals and removal helpers

```python
# Only send when condition holds
ray().if_(lambda: user.is_admin).send("admin only")

# Or provide a callback to run when condition is true
ray().if_(user.is_admin, lambda r: r.green().text("admin"))

# Deprecated-style helpers (still provided):
ray("maybe").show_when(lambda: condition)
ray("maybe").remove_when(lambda: should_hide)
```

### Working with objects (JSON + introspection)

`ray().object(obj)` tries to convert arbitrary Python objects into JSON and send a pretty-printed JSON string to Ray/Buggregator.

It will try, in order:

- `dataclasses.asdict(obj)` for dataclasses,
- `obj.model_dump()` for Pydantic v2 models,
- `obj.dict()` for Pydantic v1 / similar APIs,
- public attributes from `obj.__dict__` (filtering out private names starting with `_`).

If conversion or JSON serialization fails, it falls back to `repr(obj)`.


user = get_user()
ray().object(user)
```

`invade()` lets you inspect attributes and method results without changing your code too much:

```python
class User:
    def __init__(self):
        self._password = "secret"
    def full_name(self):
        return "Ada Lovelace"

user = User()

ray().invade(user)._password        # sends the value of _password
ray().invade(user).full_name()      # calls method and sends result
```

## Using from another local project

If you have another local project and want to use this Python client without publishing to PyPI yet, you can install from the path to this repo:

```bash
pip install -e /path/to/ray[dev]
```

Then in that other project:

```python
from python_ray import ray

ray("from another project")
```

As long as the Ray desktop app is running and configuration (if any) is correct, you should see messages in Ray.

## Contributing

Bug reports and pull requests are welcome at <https://github.com/axro-gmbh/ray>.

For Python changes, please keep the public API of `python_ray` aligned with the original PHP Ray client where it makes sense, and update the README and `pyproject.toml` when behavior or supported Python versions change.