rubyx-py 0.1.0
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.
- checksums.yaml +7 -0
- data/Cargo.toml +19 -0
- data/README.md +469 -0
- data/ext/rubyx/Cargo.toml +19 -0
- data/ext/rubyx/extconf.rb +22 -0
- data/ext/rubyx/src/async_gen.rs +1298 -0
- data/ext/rubyx/src/context.rs +812 -0
- data/ext/rubyx/src/convert.rs +1498 -0
- data/ext/rubyx/src/eval.rs +377 -0
- data/ext/rubyx/src/exception.rs +184 -0
- data/ext/rubyx/src/future.rs +126 -0
- data/ext/rubyx/src/import.rs +34 -0
- data/ext/rubyx/src/lib.rs +4212 -0
- data/ext/rubyx/src/nonblocking_stream.rs +1422 -0
- data/ext/rubyx/src/pipe_notify.rs +232 -0
- data/ext/rubyx/src/python/sync_adapter.py +31 -0
- data/ext/rubyx/src/python_api.rs +6029 -0
- data/ext/rubyx/src/python_ffi.rs +18 -0
- data/ext/rubyx/src/python_finder.rs +119 -0
- data/ext/rubyx/src/python_guard.rs +25 -0
- data/ext/rubyx/src/ruby_helpers.rs +74 -0
- data/ext/rubyx/src/rubyx_object.rs +1931 -0
- data/ext/rubyx/src/rubyx_stream.rs +950 -0
- data/ext/rubyx/src/stream.rs +713 -0
- data/ext/rubyx/src/test_helpers.rs +351 -0
- data/lib/generators/rubyx/install_generator.rb +24 -0
- data/lib/generators/rubyx/templates/rubyx_initializer.rb +17 -0
- data/lib/rubyx/context.rb +27 -0
- data/lib/rubyx/error.rb +30 -0
- data/lib/rubyx/rails.rb +105 -0
- data/lib/rubyx/railtie.rb +20 -0
- data/lib/rubyx/uv.rb +261 -0
- data/lib/rubyx/version.rb +4 -0
- data/lib/rubyx-py.rb +1 -0
- data/lib/rubyx.rb +136 -0
- metadata +123 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 9296be89cdf78ab0e25656b6643c5a7c407157d7e2767c8707fdf79a5369bdc8
|
|
4
|
+
data.tar.gz: 1f60ada7b7c16f048b5f8cd259526d7e5534b70fd7d53a6efa33d8ac0cb7c6c4
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 60ffe57e3abbb175e595c9c26bf0a8128fa872d358dfa9a121d267b6e54dcd00ad4ac413071540ed0c050b6d322eeac93cdbe191a94d81f7049e315398e910df
|
|
7
|
+
data.tar.gz: 8095f4af8147e579aadc3bd928886379070292ab01fbee68c69259caeefeaeaefb87970f501fba47a51ebe2192f5ed685c1455c88ee5f761a7cc02493b36acc6
|
data/Cargo.toml
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[workspace.package]
|
|
2
|
+
version = "0.1.0"
|
|
3
|
+
edition = "2021"
|
|
4
|
+
description = "Ruby-Python bridge powered by Rust"
|
|
5
|
+
license = "MIT"
|
|
6
|
+
|
|
7
|
+
[workspace]
|
|
8
|
+
members = ["ext/rubyx"]
|
|
9
|
+
resolver = "2"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
[workspace.dependencies]
|
|
13
|
+
magnus = { version = "0.8", features = ["embed"] }
|
|
14
|
+
libloading = "0.9"
|
|
15
|
+
libc = "1.0.0-alpha.3"
|
|
16
|
+
thiserror = "2.0"
|
|
17
|
+
rb-sys = { version = "0.9", features = ["link-ruby"] }
|
|
18
|
+
crossbeam-channel = "0.5"
|
|
19
|
+
serial_test = "3.4.0"
|
data/README.md
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# Rubyx-py
|
|
4
|
+
|
|
5
|
+
**Call Python from Ruby. No microservices, no REST APIs, no serialization overhead.**
|
|
6
|
+
|
|
7
|
+
Powered by Rust for safety and performance. Built for Rails.
|
|
8
|
+
|
|
9
|
+
[](https://badge.fury.io/rb/rubyx)
|
|
10
|
+
[](https://github.com/yinho999/rubyx/actions/workflows/ci.yml)
|
|
11
|
+
[](https://opensource.org/licenses/MIT)
|
|
12
|
+
[](https://www.ruby-lang.org)
|
|
13
|
+
[](https://www.rust-lang.org)
|
|
14
|
+
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
np = Rubyx.import('numpy')
|
|
21
|
+
np.array([1, 2, 3]).mean().to_ruby # => 2.0
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
Rubyx.eval("sum(items)", items: [1, 2, 3, 4]) # => 10
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
# Stream LLM tokens in real-time
|
|
30
|
+
Rubyx.stream(llm.generate("Tell me about Ruby")).each { |token| print token }
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
# Non-blocking — Ruby stays free while Python works
|
|
35
|
+
future = Rubyx.async_await("model.predict(data)", data: [1, 2, 3])
|
|
36
|
+
do_other_work()
|
|
37
|
+
result = future.value # get result when ready
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Built with non-blocking in mind
|
|
41
|
+
|
|
42
|
+
- **`Rubyx.stream`** / **`Rubyx.nb_stream`** — release Ruby's GVL during iteration, other threads and Fibers keep
|
|
43
|
+
running
|
|
44
|
+
- **`Rubyx.async_await`** — spawns Python on background threads, returns a `Future` immediately
|
|
45
|
+
- **`Rubyx.await`** — blocks only when you choose to
|
|
46
|
+
|
|
47
|
+
Ideal for LLM streaming, ML inference, data pipelines, and high-concurrency Rails apps.
|
|
48
|
+
|
|
49
|
+
## Install
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
# Gemfile
|
|
53
|
+
gem 'rubyx-py'
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Python is auto-managed by [uv](https://github.com/astral-sh/uv). No manual install needed.
|
|
57
|
+
|
|
58
|
+
## Rails Setup Simple Example
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
rails generate rubyx:install
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Creates `config/initializers/rubyx.rb`, `pyproject.toml`, and `app/python/`.
|
|
65
|
+
|
|
66
|
+
### Configuration
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
# config/initializers/rubyx.rb
|
|
70
|
+
Rubyx::Rails.configure do |config|
|
|
71
|
+
config.pyproject_path = Rails.root.join('pyproject.toml')
|
|
72
|
+
config.auto_init = true
|
|
73
|
+
config.python_paths = [Rails.root.join('app/python').to_s]
|
|
74
|
+
end
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Python dependencies
|
|
78
|
+
|
|
79
|
+
```toml
|
|
80
|
+
# pyproject.toml
|
|
81
|
+
[project]
|
|
82
|
+
name = "my-app"
|
|
83
|
+
version = "0.1.0"
|
|
84
|
+
requires-python = ">=3.12"
|
|
85
|
+
dependencies = []
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 1. Sync — call a Python function
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
# app/python/hello.py
|
|
92
|
+
def greet(name):
|
|
93
|
+
return f"Hello, {name}!"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
class GreetingsController < ApplicationController
|
|
98
|
+
def show
|
|
99
|
+
hello = Rubyx.import('hello')
|
|
100
|
+
render json: { message: hello.greet(params[:name]).to_ruby }
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### 2. Streaming — iterate a Python generator
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
# app/python/count.py
|
|
109
|
+
def count_up(n):
|
|
110
|
+
for i in range(n):
|
|
111
|
+
yield f"Step {i + 1}"
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
class CountController < ApplicationController
|
|
116
|
+
include ActionController::Live
|
|
117
|
+
|
|
118
|
+
def stream
|
|
119
|
+
response.headers['Content-Type'] = 'text/event-stream'
|
|
120
|
+
|
|
121
|
+
counter = Rubyx.import('count')
|
|
122
|
+
Rubyx.stream(counter.count_up(5)).each do |msg|
|
|
123
|
+
response.stream.write("data: #{msg}\n\n")
|
|
124
|
+
end
|
|
125
|
+
ensure
|
|
126
|
+
response.stream.close
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### 3. Async — non-blocking Python calls
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
# app/python/tasks.py
|
|
135
|
+
import asyncio
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
async def delayed_greet(name, seconds=1):
|
|
139
|
+
await asyncio.sleep(seconds)
|
|
140
|
+
return f"Hello, {name}! (after {seconds}s)"
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
class TasksController < ApplicationController
|
|
145
|
+
def show
|
|
146
|
+
tasks = Rubyx.import('tasks')
|
|
147
|
+
|
|
148
|
+
# Non-blocking — returns a Future immediately
|
|
149
|
+
future = Rubyx.async_await(tasks.delayed_greet(params[:name], seconds: 2))
|
|
150
|
+
do_other_work()
|
|
151
|
+
render json: { message: future.value.to_ruby }
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Rails Setup Advanced LLM Example
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
rails generate rubyx:install
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Creates `config/initializers/rubyx.rb`, `pyproject.toml`, and `app/python/`.
|
|
163
|
+
|
|
164
|
+
### Configuration
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
# config/initializers/rubyx.rb
|
|
168
|
+
Rubyx::Rails.configure do |config|
|
|
169
|
+
config.pyproject_path = Rails.root.join('pyproject.toml')
|
|
170
|
+
config.auto_init = true
|
|
171
|
+
config.python_paths = [Rails.root.join('app/python').to_s]
|
|
172
|
+
end
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Python dependencies
|
|
176
|
+
|
|
177
|
+
```toml
|
|
178
|
+
# pyproject.toml
|
|
179
|
+
[project]
|
|
180
|
+
name = "my-app"
|
|
181
|
+
version = "0.1.0"
|
|
182
|
+
requires-python = ">=3.12"
|
|
183
|
+
dependencies = ["numpy", "pandas", "transformers"]
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Write Python code
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
# app/python/services/text_processing.py
|
|
190
|
+
class TextAnalyzer:
|
|
191
|
+
def __init__(self, text):
|
|
192
|
+
self.text = text
|
|
193
|
+
self._words = text.split()
|
|
194
|
+
|
|
195
|
+
def summary(self):
|
|
196
|
+
return {
|
|
197
|
+
"word_count": len(self._words),
|
|
198
|
+
"unique_words": len(set(self._words)),
|
|
199
|
+
"avg_word_length": round(
|
|
200
|
+
sum(len(w) for w in self._words) / max(len(self._words), 1), 2
|
|
201
|
+
),
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Call it from Rails
|
|
206
|
+
|
|
207
|
+
```ruby
|
|
208
|
+
|
|
209
|
+
class AnalysisController < ApplicationController
|
|
210
|
+
def analyze
|
|
211
|
+
tp = Rubyx.import('services.text_processing')
|
|
212
|
+
analyzer = tp.TextAnalyzer(params[:text])
|
|
213
|
+
render json: analyzer.summary.to_ruby
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### SSE streaming (LLM-style)
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
# app/python/services/llm.py
|
|
222
|
+
from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer
|
|
223
|
+
import threading
|
|
224
|
+
|
|
225
|
+
_model, _tokenizer = None, None
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def load_model(name="Qwen/Qwen2.5-0.5B-Instruct"):
|
|
229
|
+
global _model, _tokenizer
|
|
230
|
+
_tokenizer = AutoTokenizer.from_pretrained(name)
|
|
231
|
+
_model = AutoModelForCausalLM.from_pretrained(name, torch_dtype="auto")
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def stream_generate(prompt, max_tokens=256):
|
|
235
|
+
messages = [{"role": "user", "content": prompt}]
|
|
236
|
+
text = _tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
|
|
237
|
+
inputs = _tokenizer([text], return_tensors="pt").to(_model.device)
|
|
238
|
+
streamer = TextIteratorStreamer(_tokenizer, skip_prompt=True, skip_special_tokens=True)
|
|
239
|
+
|
|
240
|
+
thread = threading.Thread(target=_model.generate,
|
|
241
|
+
kwargs={**inputs, "max_new_tokens": max_tokens, "streamer": streamer})
|
|
242
|
+
thread.start()
|
|
243
|
+
for token in streamer:
|
|
244
|
+
if token:
|
|
245
|
+
yield token
|
|
246
|
+
thread.join()
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
```ruby
|
|
250
|
+
# config/initializers/rubyx.rb
|
|
251
|
+
Rubyx::Rails.configure do |config|
|
|
252
|
+
config.pyproject_path = Rails.root.join('pyproject.toml')
|
|
253
|
+
config.auto_init = true
|
|
254
|
+
config.python_paths = [Rails.root.join('app/python').to_s]
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Load models once at boot — must be inside after_initialize
|
|
258
|
+
# so Python is already initialized by the Railtie
|
|
259
|
+
Rails.application.config.after_initialize do
|
|
260
|
+
llm = Rubyx.import('services.llm')
|
|
261
|
+
llm.load_model("Qwen/Qwen2.5-0.5B-Instruct")
|
|
262
|
+
end
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
```ruby
|
|
266
|
+
class ChatController < ApplicationController
|
|
267
|
+
include ActionController::Live
|
|
268
|
+
|
|
269
|
+
def stream
|
|
270
|
+
llm = Rubyx.import('services.llm')
|
|
271
|
+
response.headers['Content-Type'] = 'text/event-stream'
|
|
272
|
+
|
|
273
|
+
Rubyx.stream(llm.stream_generate(params[:prompt])).each do |token|
|
|
274
|
+
token_str = token.to_s.gsub("\n", "\\n")
|
|
275
|
+
response.stream.write("data: #{token_str}\n\n")
|
|
276
|
+
end
|
|
277
|
+
response.stream.write("data: [DONE]\n\n")
|
|
278
|
+
ensure
|
|
279
|
+
response.stream.close
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Rake tasks
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
rake rubyx:init # Initialize Python environment
|
|
288
|
+
rake rubyx:status # Check environment status
|
|
289
|
+
rake rubyx:packages # List installed Python packages
|
|
290
|
+
rake rubyx:clear_cache # Clear cached environments
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## Standalone Setup
|
|
294
|
+
|
|
295
|
+
```ruby
|
|
296
|
+
require 'rubyx'
|
|
297
|
+
|
|
298
|
+
Rubyx.uv_init <<~TOML
|
|
299
|
+
[project]
|
|
300
|
+
name = "my-script"
|
|
301
|
+
version = "0.1.0"
|
|
302
|
+
requires-python = ">=3.12"
|
|
303
|
+
dependencies = ["numpy"]
|
|
304
|
+
TOML
|
|
305
|
+
|
|
306
|
+
np = Rubyx.import('numpy')
|
|
307
|
+
np.array([1, 2, 3, 4, 5]).std().to_ruby # => 1.4142135623730951
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## Eval with Globals
|
|
311
|
+
|
|
312
|
+
Pass Ruby data directly into Python:
|
|
313
|
+
|
|
314
|
+
```ruby
|
|
315
|
+
Rubyx.eval("x ** 2 + y ** 2", x: 3, y: 4).to_ruby # => 25
|
|
316
|
+
Rubyx.eval("f'Hello, {name}!'", name: "World").to_ruby # => "Hello, World!"
|
|
317
|
+
Rubyx.eval("max(items)", items: [3, 1, 4, 1, 5]).to_ruby # => 5
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Supports: Integer, Float, String, Symbol, Bool, nil, Array, Hash, and RubyxObject.
|
|
321
|
+
|
|
322
|
+
## Python Objects
|
|
323
|
+
|
|
324
|
+
Python objects are wrapped as `RubyxObject`:
|
|
325
|
+
|
|
326
|
+
```ruby
|
|
327
|
+
os = Rubyx.import('os')
|
|
328
|
+
os.getcwd().to_ruby # => "/home/user"
|
|
329
|
+
os.path.exists("/tmp").to_ruby # => true
|
|
330
|
+
|
|
331
|
+
# Subscript access
|
|
332
|
+
d = Rubyx.eval("{'a': 1, 'b': 2}")
|
|
333
|
+
d['a'].to_ruby # => 1
|
|
334
|
+
d['c'] = 3
|
|
335
|
+
|
|
336
|
+
# Enumerable
|
|
337
|
+
py_list = Rubyx.eval("[1, 2, 3, 4, 5]")
|
|
338
|
+
py_list.map { |x| x.to_ruby * 2 } # => [2, 4, 6, 8, 10]
|
|
339
|
+
py_list.select { |x| x.to_ruby > 3 } # filtered RubyxObjects
|
|
340
|
+
|
|
341
|
+
# Introspection
|
|
342
|
+
py_list.truthy? # => true
|
|
343
|
+
py_list.callable? # => false
|
|
344
|
+
py_list.py_type # => "list"
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
## Context
|
|
348
|
+
|
|
349
|
+
Persistent state across multiple eval calls:
|
|
350
|
+
|
|
351
|
+
```ruby
|
|
352
|
+
ctx = Rubyx.context
|
|
353
|
+
|
|
354
|
+
ctx.eval("import math")
|
|
355
|
+
ctx.eval("data = [1, 2, 3, 4, 5]")
|
|
356
|
+
ctx.eval("avg = sum(data) / len(data)")
|
|
357
|
+
ctx.eval("avg").to_ruby # => 3.0
|
|
358
|
+
|
|
359
|
+
# Inject Ruby data into context
|
|
360
|
+
ctx.eval("total = base + offset", base: 100, offset: 42)
|
|
361
|
+
ctx.eval("total").to_ruby # => 142
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
## Streaming
|
|
365
|
+
|
|
366
|
+
```ruby
|
|
367
|
+
gen = Rubyx.eval("(x ** 2 for x in range(5))")
|
|
368
|
+
Rubyx.stream(gen).each { |val| puts val } # 0, 1, 4, 9, 16
|
|
369
|
+
|
|
370
|
+
# Non-blocking (releases GVL for other Ruby threads)
|
|
371
|
+
Rubyx.nb_stream(gen).each { |val| process(val) }
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
## Async / Await
|
|
375
|
+
|
|
376
|
+
```ruby
|
|
377
|
+
ctx = Rubyx.context
|
|
378
|
+
ctx.eval("import asyncio")
|
|
379
|
+
ctx.eval("async def fetch(url): ...")
|
|
380
|
+
|
|
381
|
+
# Blocking
|
|
382
|
+
result = ctx.await("fetch(url)", url: "https://example.com")
|
|
383
|
+
|
|
384
|
+
# Non-blocking (returns Future)
|
|
385
|
+
future = ctx.async_await("fetch(url)", url: "https://example.com")
|
|
386
|
+
do_other_stuff()
|
|
387
|
+
result = future.value # blocks only when needed
|
|
388
|
+
future.ready? # check without blocking
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
## Error Handling
|
|
392
|
+
|
|
393
|
+
Python exceptions map to Ruby classes:
|
|
394
|
+
|
|
395
|
+
```ruby
|
|
396
|
+
|
|
397
|
+
begin
|
|
398
|
+
Rubyx.eval('{}["missing"]')
|
|
399
|
+
rescue Rubyx::KeyError => e
|
|
400
|
+
puts e.message # includes Python traceback
|
|
401
|
+
end
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
| Python | Ruby |
|
|
405
|
+
|---------------------------------------|-------------------------------|
|
|
406
|
+
| `KeyError` | `Rubyx::KeyError` |
|
|
407
|
+
| `IndexError` | `Rubyx::IndexError` |
|
|
408
|
+
| `ValueError` | `Rubyx::ValueError` |
|
|
409
|
+
| `TypeError` | `Rubyx::TypeError` |
|
|
410
|
+
| `AttributeError` | `Rubyx::AttributeError` |
|
|
411
|
+
| `ImportError` / `ModuleNotFoundError` | `Rubyx::ImportError` |
|
|
412
|
+
| `SyntaxError` | `SyntaxError` (Ruby built-in) |
|
|
413
|
+
| Everything else | `Rubyx::PythonError` |
|
|
414
|
+
|
|
415
|
+
All inherit from `Rubyx::Error` (`StandardError`).
|
|
416
|
+
|
|
417
|
+
## Local Python Files
|
|
418
|
+
|
|
419
|
+
```python
|
|
420
|
+
# app/python/services/analyzer.py
|
|
421
|
+
class Analyzer:
|
|
422
|
+
def __init__(self, data):
|
|
423
|
+
self.data = data
|
|
424
|
+
|
|
425
|
+
def summary(self):
|
|
426
|
+
return {"count": len(self.data), "sum": sum(self.data)}
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
```ruby
|
|
430
|
+
svc = Rubyx.import('services.analyzer')
|
|
431
|
+
svc.Analyzer([1, 2, 3]).summary.to_ruby # => {"count" => 3, "sum" => 6}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
## API Reference
|
|
435
|
+
|
|
436
|
+
| Method | Description |
|
|
437
|
+
|--------------------------------------|---------------------------------|
|
|
438
|
+
| `Rubyx.uv_init(toml, **opts)` | Setup Python env and initialize |
|
|
439
|
+
| `Rubyx.import(name)` | Import a Python module |
|
|
440
|
+
| `Rubyx.eval(code, **globals)` | Evaluate Python code |
|
|
441
|
+
| `Rubyx.await(code, **globals)` | Run async code (blocking) |
|
|
442
|
+
| `Rubyx.async_await(code, **globals)` | Run async code (returns Future) |
|
|
443
|
+
| `Rubyx.stream(iterable)` | Stream a Python generator |
|
|
444
|
+
| `Rubyx.nb_stream(iterable)` | Non-blocking stream (GVL-aware) |
|
|
445
|
+
| `Rubyx.context` | Create isolated Python context |
|
|
446
|
+
| `Rubyx.initialized?` | Check if Python is ready |
|
|
447
|
+
|
|
448
|
+
| RubyxObject | |
|
|
449
|
+
|--------------------------|-------------------------------|
|
|
450
|
+
| `.to_ruby` | Convert to native Ruby type |
|
|
451
|
+
| `.to_s` / `.inspect` | String / repr |
|
|
452
|
+
| `.method_missing` | Delegates to Python |
|
|
453
|
+
| `[]` / `[]=` / `.delete` | Subscript access |
|
|
454
|
+
| `.each` | Iterate (includes Enumerable) |
|
|
455
|
+
| `.truthy?` / `.falsy?` | Python truthiness |
|
|
456
|
+
| `.callable?` | Check if callable |
|
|
457
|
+
| `.py_type` | Python type name |
|
|
458
|
+
|
|
459
|
+
## Requirements
|
|
460
|
+
|
|
461
|
+
- Ruby >= 3.0
|
|
462
|
+
- Rust (for building from source)
|
|
463
|
+
- Python >= 3.12 (auto-managed by uv)
|
|
464
|
+
|
|
465
|
+
Precompiled gems available for Linux and macOS (x86_64 and ARM64).
|
|
466
|
+
|
|
467
|
+
## License
|
|
468
|
+
|
|
469
|
+
MIT
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "rubyx"
|
|
3
|
+
version.workspace = true
|
|
4
|
+
edition.workspace = true
|
|
5
|
+
publish = false
|
|
6
|
+
|
|
7
|
+
[lib]
|
|
8
|
+
crate-type = ["cdylib"]
|
|
9
|
+
|
|
10
|
+
[dependencies]
|
|
11
|
+
magnus = { workspace = true }
|
|
12
|
+
libloading = { workspace = true }
|
|
13
|
+
libc = { workspace = true }
|
|
14
|
+
thiserror = { workspace = true }
|
|
15
|
+
rb-sys = { workspace = true }
|
|
16
|
+
crossbeam-channel = { workspace = true }
|
|
17
|
+
|
|
18
|
+
[dev-dependencies]
|
|
19
|
+
serial_test = { workspace = true }
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require 'mkmf'
|
|
2
|
+
|
|
3
|
+
unless respond_to?(:dummy_makefile)
|
|
4
|
+
def dummy_makefile(dir)
|
|
5
|
+
["SHELL = /bin/sh",
|
|
6
|
+
"ECHO = @echo",
|
|
7
|
+
"Q = @",
|
|
8
|
+
"TOUCH = touch",
|
|
9
|
+
"COPY = cp",
|
|
10
|
+
"RM_RF = rm -rf",
|
|
11
|
+
"MAKEDIRS = mkdir -p",
|
|
12
|
+
"INSTALL_PROG = install -m 0755",
|
|
13
|
+
"all install static install-so install-rb:",
|
|
14
|
+
"pre-install-rb: install-rb",
|
|
15
|
+
"clean distclean realclean:",
|
|
16
|
+
"\t@-$(RM_RF) mkmf.log"]
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
require 'rb_sys/mkmf'
|
|
21
|
+
|
|
22
|
+
create_rust_makefile('rubyx/rubyx')
|