rubyx-py 0.1.1-aarch64-linux → 0.2.1-aarch64-linux

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 78e55da43621514d0e7e8457b19a61a11940c227acbc58e2b2bc74cb69f02c14
4
- data.tar.gz: eaa68df2386ad15bbb8330ca66e3d4c226e9b5705af0c2c36f1eca44040f274a
3
+ metadata.gz: 25cb549b764a4c233144af01c9fe0fc86709ba1c9c6bbc8ac01a4f60f030ffe6
4
+ data.tar.gz: 812f62a87103e7c52e2503054942645aaa6e9e5f5c0ddf63d20a484c6f9da1b2
5
5
  SHA512:
6
- metadata.gz: 27fb43d1eb60920ca4cd78b65e682200c4f5a0297a1ecb3b258b77a6ed6971957ab3e94184df168e185752181bcb74493f686a25acff8931a67773f39af145e1
7
- data.tar.gz: 7bd77196083a87066eb1e384bbe2df8f49dae64995028bc58ee327fc646a80ce6a110674593d114ee383c421321de0adbc593dd2cafb0c2e5fb8cfe7290075fd
6
+ metadata.gz: ea4882957a0522567d2dbdfe7c9a64b0d7bfa23522928d7bb39d22c763b5d936d348b1dfa762e4197bd67174aa3d2e4a26928f0586c93bc267d7d56ca02a7563
7
+ data.tar.gz: 34b00a5cdcf9347ec8a812ac30ad7e6142ff435768e66d582c0e6125632a637ae136b813f1ffba4af3b3c19779674968e088f5014bfa9383a8c76a9c9be437bc
data/README.md CHANGED
@@ -1,17 +1,21 @@
1
1
  <div align="center">
2
2
 
3
+ <img src="docs/assets/logo.png" alt="rubyx-py" width="200">
4
+
3
5
  # Rubyx-py
4
6
 
5
7
  **Call Python from Ruby. No microservices, no REST APIs, no serialization overhead.**
6
8
 
7
- Powered by Rust for safety and performance. Built for Rails.
9
+ Powered by Rust for safety and performance. Built for Rails. Inspired by [Pythonx](https://github.com/livebook-dev/pythonx)
8
10
 
9
- [![Gem Version](https://badge.fury.io/rb/rubyx.svg)](https://badge.fury.io/rb/rubyx)
11
+ [![Gem Version](https://badge.fury.io/rb/rubyx-py.svg)](https://badge.fury.io/rb/rubyx-py)
10
12
  [![CI](https://github.com/yinho999/rubyx/actions/workflows/ci.yml/badge.svg)](https://github.com/yinho999/rubyx/actions/workflows/ci.yml)
11
13
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
12
14
  [![Ruby](https://img.shields.io/badge/Ruby-%3E%3D%203.0-red.svg)](https://www.ruby-lang.org)
13
15
  [![Rust](https://img.shields.io/badge/Rust-powered-orange.svg)](https://www.rust-lang.org)
14
-
16
+ > *"Rubyx showed that the proxy-object pattern works beautifully for cross-language bridges in Rust, and its magnus + rb-sys architecture is exactly what [Boax](https://intertwingly.net/blog/2026/03/25/Calling-JavaScript-from-Ruby.html) uses."*
17
+ >
18
+ > — [Sam Ruby](https://en.wikipedia.org/wiki/Sam_Ruby), author of *Agile Web Development with Rails*
15
19
  </div>
16
20
 
17
21
  ---
@@ -34,15 +38,17 @@ Rubyx.stream(llm.generate("Tell me about Ruby")).each { |token| print token }
34
38
  # Non-blocking — Ruby stays free while Python works
35
39
  future = Rubyx.async_await("model.predict(data)", data: [1, 2, 3])
36
40
  do_other_work()
37
- result = future.value # get result when ready
41
+ result = future.await # GVL released during wait, reacquired when ready
38
42
  ```
39
43
 
40
44
  ### Built with non-blocking in mind
41
45
 
42
46
  - **`Rubyx.stream`** / **`Rubyx.nb_stream`** — release Ruby's GVL during iteration, other threads and Fibers keep
43
47
  running
44
- - **`Rubyx.async_await`** — spawns Python on background threads, returns a `Future` immediately
45
- - **`Rubyx.await`** blocks only when you choose to
48
+ - **`Rubyx.async_await`** — spawns Python on background threads, returns a `Future` immediately; `future.await` releases
49
+ the GVL while waiting, reacquires when ready
50
+ - **`Rubyx.await`** — GVL released while waiting; returns native Ruby types for primitives, `RubyxObject` for complex
51
+ Python objects
46
52
 
47
53
  Ideal for LLM streaming, ML inference, data pipelines, and high-concurrency Rails apps.
48
54
 
@@ -88,20 +94,27 @@ dependencies = []
88
94
  ### 1. Sync — call a Python function
89
95
 
90
96
  ```python
91
- # app/python/hello.py
92
- def greet(name):
93
- return f"Hello, {name}!"
97
+ # app/python/example.py
98
+ def hello(name):
99
+ return f"Hello, {name}! From Python."
94
100
  ```
95
101
 
96
102
  ```ruby
103
+
97
104
  class GreetingsController < ApplicationController
98
- def show
99
- hello = Rubyx.import('hello')
100
- render json: { message: hello.greet(params[:name]).to_ruby }
105
+ def index
106
+ example = Rubyx.import('example')
107
+ render json: { message: example.hello(params[:name] || 'World').to_ruby }
101
108
  end
102
109
  end
103
110
  ```
104
111
 
112
+ ```ruby
113
+ Rails.application.routes.draw do
114
+ root "greetings#index"
115
+ end
116
+ ```
117
+
105
118
  ### 2. Streaming — iterate a Python generator
106
119
 
107
120
  ```python
@@ -112,6 +125,7 @@ def count_up(n):
112
125
  ```
113
126
 
114
127
  ```ruby
128
+
115
129
  class CountController < ApplicationController
116
130
  include ActionController::Live
117
131
 
@@ -141,14 +155,15 @@ async def delayed_greet(name, seconds=1):
141
155
  ```
142
156
 
143
157
  ```ruby
158
+
144
159
  class TasksController < ApplicationController
145
160
  def show
146
161
  tasks = Rubyx.import('tasks')
147
162
 
148
163
  # Non-blocking — returns a Future immediately
149
- future = Rubyx.async_await(tasks.delayed_greet(params[:name], seconds: 2))
164
+ future = Rubyx.async_await(tasks.delayed_greet(params[:name] || 'World', seconds: 2))
150
165
  do_other_work()
151
- render json: { message: future.value.to_ruby }
166
+ render json: { message: future.await.to_ruby }
152
167
  end
153
168
  end
154
169
  ```
@@ -263,6 +278,7 @@ end
263
278
  ```
264
279
 
265
280
  ```ruby
281
+
266
282
  class ChatController < ApplicationController
267
283
  include ActionController::Live
268
284
 
@@ -317,7 +333,29 @@ Rubyx.eval("f'Hello, {name}!'", name: "World").to_ruby # => "Hello, World!"
317
333
  Rubyx.eval("max(items)", items: [3, 1, 4, 1, 5]).to_ruby # => 5
318
334
  ```
319
335
 
320
- Supports: Integer, Float, String, Symbol, Bool, nil, Array, Hash, and RubyxObject.
336
+ Supports: Integer, Float, String, Symbol, Bool, nil, Array, Hash, binary String (ASCII-8BIT), and RubyxObject.
337
+
338
+ ## Bytes / Binary Data
339
+
340
+ Python `bytes` and `bytearray` convert to Ruby `String` with `ASCII-8BIT` encoding. Ruby binary strings (`.b`) convert to Python `bytes`:
341
+
342
+ ```ruby
343
+ # Python bytes → Ruby
344
+ Rubyx.eval("b'hello'").to_ruby # => "hello" (ASCII-8BIT)
345
+ Rubyx.eval("bytearray(b'hello')").to_ruby # => "hello" (ASCII-8BIT)
346
+
347
+ # Ruby → Python
348
+ ctx = Rubyx.context
349
+ ctx.eval("type(data).__name__", data: "hello".b) # => "bytes"
350
+ ctx.eval("type(data).__name__", data: "hello") # => "str"
351
+
352
+ # Roundtrip binary data
353
+ ctx.eval("data", data: "\xff\x00\xfe".b).to_ruby # => "\xFF\x00\xFE" (ASCII-8BIT)
354
+
355
+ # Works with Python stdlib
356
+ ctx.eval("import base64")
357
+ ctx.eval("base64.b64encode(raw)", raw: "Hello".b).to_ruby # => "SGVsbG8=" (ASCII-8BIT)
358
+ ```
321
359
 
322
360
  ## Python Objects
323
361
 
@@ -378,13 +416,13 @@ ctx = Rubyx.context
378
416
  ctx.eval("import asyncio")
379
417
  ctx.eval("async def fetch(url): ...")
380
418
 
381
- # Blocking
419
+ # GVL released while waiting, reacquired when ready
382
420
  result = ctx.await("fetch(url)", url: "https://example.com")
383
421
 
384
422
  # Non-blocking (returns Future)
385
423
  future = ctx.async_await("fetch(url)", url: "https://example.com")
386
424
  do_other_stuff()
387
- result = future.value # blocks only when needed
425
+ result = future.await # GVL released during wait, reacquired when ready
388
426
  future.ready? # check without blocking
389
427
  ```
390
428
 
@@ -433,17 +471,17 @@ svc.Analyzer([1, 2, 3]).summary.to_ruby # => {"count" => 3, "sum" => 6}
433
471
 
434
472
  ## API Reference
435
473
 
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 |
474
+ | Method | Description |
475
+ |--------------------------------------|-----------------------------------------------|
476
+ | `Rubyx.uv_init(toml, **opts)` | Setup Python env and initialize |
477
+ | `Rubyx.import(name)` | Import a Python module |
478
+ | `Rubyx.eval(code, **globals)` | Evaluate Python code |
479
+ | `Rubyx.await(code, **globals)` | Run async code (GVL released while waiting) |
480
+ | `Rubyx.async_await(code, **globals)` | Run async code (non-blocking, returns Future) |
481
+ | `Rubyx.stream(iterable)` | Stream a Python generator |
482
+ | `Rubyx.nb_stream(iterable)` | Non-blocking stream (GVL-aware) |
483
+ | `Rubyx.context` | Create isolated Python context |
484
+ | `Rubyx.initialized?` | Check if Python is ready |
447
485
 
448
486
  | RubyxObject | |
449
487
  |--------------------------|-------------------------------|
@@ -456,6 +494,37 @@ svc.Analyzer([1, 2, 3]).summary.to_ruby # => {"count" => 3, "sum" => 6}
456
494
  | `.callable?` | Check if callable |
457
495
  | `.py_type` | Python type name |
458
496
 
497
+ ## Type Conversion
498
+
499
+ | Python | Ruby | Notes |
500
+ |-----------------------|------------------------------|------------------------|
501
+ | `int` | `Integer` | |
502
+ | `float` | `Float` | |
503
+ | `str` | `String` (UTF-8) | |
504
+ | `bytes` | `String` (ASCII-8BIT) | binary data |
505
+ | `bytearray` | `String` (ASCII-8BIT) | binary data |
506
+ | `bool` | `true` / `false` | |
507
+ | `None` | `nil` | |
508
+ | `list` / `tuple` | `Array` | |
509
+ | `dict` | `Hash` | |
510
+ | `set` / `frozenset` | `Array` | |
511
+ | everything else | `RubyxObject` | proxy to Python object |
512
+
513
+ **Ruby → Python** (via globals/kwargs):
514
+
515
+ | Ruby | Python |
516
+ |--------------------------------|-------------|
517
+ | `Integer` | `int` |
518
+ | `Float` | `float` |
519
+ | `String` (UTF-8) | `str` |
520
+ | `String` (ASCII-8BIT / `.b`) | `bytes` |
521
+ | `true` / `false` | `bool` |
522
+ | `nil` | `None` |
523
+ | `Array` | `list` |
524
+ | `Hash` | `dict` |
525
+ | `Symbol` | `str` |
526
+ | `RubyxObject` | original |
527
+
459
528
  ## Requirements
460
529
 
461
530
  - Ruby >= 3.0
Binary file
Binary file
Binary file
Binary file
Binary file
data/lib/rubyx/railtie.rb CHANGED
@@ -14,7 +14,8 @@ module Rubyx
14
14
 
15
15
  # Register rake tasks
16
16
  rake_tasks do
17
- load 'rubyx/tasks/rubyx.rake'
17
+ task_file = File.expand_path('tasks/rubyx.rake', __dir__)
18
+ load task_file if File.exist?(task_file)
18
19
  end
19
20
  end
20
21
  end
@@ -0,0 +1,127 @@
1
+ namespace :rubyx do
2
+ desc 'Initialize Python environment (downloads uv and Python if needed)'
3
+ task init: :environment do
4
+ Rubyx::Rails.init!
5
+ puts '[Rubyx] Python environment initialized successfully.'
6
+ end
7
+
8
+ desc 'Check Python environment health'
9
+ task check: :environment do
10
+ puts 'Checking Python environment...'
11
+ puts
12
+
13
+ # Check uv
14
+ system_uv = `which uv 2>/dev/null`.strip
15
+ uv_available = !system_uv.empty? && File.exist?(system_uv)
16
+ puts "uv available: #{uv_available ? "Yes (#{system_uv})" : 'No (will auto-download)'}"
17
+
18
+ begin
19
+ Rubyx::Rails.ensure_initialized!
20
+ puts 'Python initialized: Yes'
21
+ rescue => e
22
+ puts "Python initialized: No (#{e.message})"
23
+ puts
24
+ puts 'Run `rake rubyx:init` to initialize.'
25
+ exit 1
26
+ end
27
+
28
+ begin
29
+ Rubyx.eval('1 + 1')
30
+ puts 'Basic eval: OK'
31
+ rescue => e
32
+ puts "Basic eval: FAILED (#{e.message})"
33
+ exit 1
34
+ end
35
+
36
+ begin
37
+ gen = Rubyx.eval("import sys\niter([sys.version.split()[0]])")
38
+ version = Rubyx.stream(gen).first
39
+ puts "Import sys: OK (Python #{version})"
40
+ rescue => e
41
+ puts "Import sys: FAILED (#{e.message})"
42
+ exit 1
43
+ end
44
+
45
+ puts
46
+ puts 'All checks passed!'
47
+ end
48
+
49
+ desc 'Show Rubyx configuration and status'
50
+ task status: :environment do
51
+ config = Rubyx::Rails.configuration
52
+
53
+ puts 'Rubyx Status'
54
+ puts '=' * 40
55
+
56
+ puts "Initialized: #{Rubyx::Rails.initialized?}"
57
+ puts
58
+
59
+ puts 'Configuration:'
60
+ puts " pyproject_path: #{config.pyproject_path || '(not set)'}"
61
+ puts " pyproject_content: #{config.pyproject_content ? '(inline, %d bytes)' % config.pyproject_content.length : '(not set)'}"
62
+ puts " auto_init: #{config.auto_init}"
63
+ puts " force_reinit: #{config.force_reinit}"
64
+ puts " uv_version: #{config.uv_version}"
65
+ puts " debug: #{config.debug}"
66
+ puts " python_paths: #{config.python_paths.inspect}"
67
+ puts " uv_path: #{config.uv_path || '(auto-download)'}"
68
+ puts " uv_args: #{config.uv_args.inspect}"
69
+ puts
70
+
71
+ if config.pyproject_path
72
+ exists = File.exist?(config.pyproject_path.to_s)
73
+ puts "pyproject.toml exists: #{exists}"
74
+ end
75
+
76
+ if config.pyproject_path
77
+ venv_dir = File.join(File.dirname(config.pyproject_path.to_s), '.venv')
78
+ puts ".venv exists: #{Dir.exist?(venv_dir)}"
79
+ end
80
+
81
+ system_uv = `which uv 2>/dev/null`.strip
82
+ puts "System uv: #{!system_uv.empty? ? system_uv : '(not found)'}"
83
+ end
84
+
85
+ desc 'List installed Python packages'
86
+ task packages: :environment do
87
+ Rubyx::Rails.ensure_initialized!
88
+
89
+ gen = Rubyx.eval(<<~PY)
90
+ import pkg_resources
91
+ packages = sorted([f"{d.project_name}=={d.version}" for d in pkg_resources.working_set])
92
+ iter(packages)
93
+ PY
94
+
95
+ puts 'Installed Python packages:'
96
+ Rubyx.stream(gen).each { |pkg| puts " #{pkg}" }
97
+ rescue => e
98
+ begin
99
+ gen = Rubyx.eval(<<~PY)
100
+ from importlib.metadata import distributions
101
+ packages = sorted([f"{d.metadata['Name']}=={d.metadata['Version']}" for d in distributions()])
102
+ iter(packages)
103
+ PY
104
+
105
+ puts 'Installed Python packages:'
106
+ Rubyx.stream(gen).each { |pkg| puts " #{pkg}" }
107
+ rescue => e2
108
+ puts "Could not list packages: #{e2.message}"
109
+ end
110
+ end
111
+
112
+ desc 'Clear the Rubyx cache (re-download uv + Python on next init)'
113
+ task clear_cache: :environment do
114
+ cache_dir = File.join(
115
+ ENV.fetch('XDG_CACHE_HOME', File.join(Dir.home, '.cache')),
116
+ 'rubyx'
117
+ )
118
+
119
+ if Dir.exist?(cache_dir)
120
+ require 'fileutils'
121
+ FileUtils.rm_rf(cache_dir)
122
+ puts "[Rubyx] Cache cleared: #{cache_dir}"
123
+ else
124
+ puts '[Rubyx] No cache directory found.'
125
+ end
126
+ end
127
+ end
data/lib/rubyx/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Rubyx
3
- VERSION = "0.1.1".freeze
4
- end
3
+ VERSION = "0.2.1".freeze
4
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubyx-py
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.1
5
5
  platform: aarch64-linux
6
6
  authors:
7
7
  - Naiker
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-03-24 00:00:00.000000000 Z
11
+ date: 2026-05-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake-compiler
@@ -47,6 +47,7 @@ extensions: []
47
47
  extra_rdoc_files: []
48
48
  files:
49
49
  - README.md
50
+ - docs/assets/logo.png
50
51
  - ext/rubyx/src/python/sync_adapter.py
51
52
  - lib/generators/rubyx/install_generator.rb
52
53
  - lib/generators/rubyx/templates/example.py
@@ -62,6 +63,7 @@ files:
62
63
  - lib/rubyx/error.rb
63
64
  - lib/rubyx/rails.rb
64
65
  - lib/rubyx/railtie.rb
66
+ - lib/rubyx/tasks/rubyx.rake
65
67
  - lib/rubyx/uv.rb
66
68
  - lib/rubyx/version.rb
67
69
  homepage: https://github.com/yinho999/rubyx