rpy-bridge 0.5.0__tar.gz → 0.5.2__tar.gz
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.
- {rpy_bridge-0.5.0/src/rpy_bridge.egg-info → rpy_bridge-0.5.2}/PKG-INFO +38 -50
- {rpy_bridge-0.5.0 → rpy_bridge-0.5.2}/README.md +37 -49
- {rpy_bridge-0.5.0 → rpy_bridge-0.5.2}/pyproject.toml +6 -6
- {rpy_bridge-0.5.0 → rpy_bridge-0.5.2}/src/rpy_bridge/core.py +35 -1
- {rpy_bridge-0.5.0 → rpy_bridge-0.5.2}/src/rpy_bridge/dataframe.py +24 -2
- {rpy_bridge-0.5.0 → rpy_bridge-0.5.2/src/rpy_bridge.egg-info}/PKG-INFO +38 -50
- {rpy_bridge-0.5.0 → rpy_bridge-0.5.2}/LICENSE +0 -0
- {rpy_bridge-0.5.0 → rpy_bridge-0.5.2}/README.rst +0 -0
- {rpy_bridge-0.5.0 → rpy_bridge-0.5.2}/setup.cfg +0 -0
- {rpy_bridge-0.5.0 → rpy_bridge-0.5.2}/src/rpy_bridge/__init__.py +0 -0
- {rpy_bridge-0.5.0 → rpy_bridge-0.5.2}/src/rpy_bridge/compare.py +0 -0
- {rpy_bridge-0.5.0 → rpy_bridge-0.5.2}/src/rpy_bridge/convert.py +0 -0
- {rpy_bridge-0.5.0 → rpy_bridge-0.5.2}/src/rpy_bridge/env.py +0 -0
- {rpy_bridge-0.5.0 → rpy_bridge-0.5.2}/src/rpy_bridge/logging.py +0 -0
- {rpy_bridge-0.5.0 → rpy_bridge-0.5.2}/src/rpy_bridge/py.typed +0 -0
- {rpy_bridge-0.5.0 → rpy_bridge-0.5.2}/src/rpy_bridge/renv.py +0 -0
- {rpy_bridge-0.5.0 → rpy_bridge-0.5.2}/src/rpy_bridge/rpy2_loader.py +0 -0
- {rpy_bridge-0.5.0 → rpy_bridge-0.5.2}/src/rpy_bridge.egg-info/SOURCES.txt +0 -0
- {rpy_bridge-0.5.0 → rpy_bridge-0.5.2}/src/rpy_bridge.egg-info/dependency_links.txt +0 -0
- {rpy_bridge-0.5.0 → rpy_bridge-0.5.2}/src/rpy_bridge.egg-info/requires.txt +0 -0
- {rpy_bridge-0.5.0 → rpy_bridge-0.5.2}/src/rpy_bridge.egg-info/top_level.txt +0 -0
- {rpy_bridge-0.5.0 → rpy_bridge-0.5.2}/tests/test_package_call.py +0 -0
- {rpy_bridge-0.5.0 → rpy_bridge-0.5.2}/tests/test_py2r.py +0 -0
- {rpy_bridge-0.5.0 → rpy_bridge-0.5.2}/tests/test_roundtrip.py +0 -0
- {rpy_bridge-0.5.0 → rpy_bridge-0.5.2}/tests/test_wrapper.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rpy-bridge
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.2
|
|
4
4
|
Summary: Python-to-R interoperability engine with environment management, type-safe conversions, data normalization, and safe R function execution.
|
|
5
5
|
Author-email: Victoria Cheung <victoriakcheung@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -59,17 +59,11 @@ Dynamic: license-file
|
|
|
59
59
|
|
|
60
60
|
# rpy-bridge
|
|
61
61
|
|
|
62
|
-
**rpy-bridge** is a Python-controlled **R execution orchestrator**
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
R code is executed when invoked from Python: project roots are inferred, `renv`
|
|
68
|
-
environments can be activated out-of-tree, relative paths behave as expected,
|
|
69
|
-
and return values are normalized for safe Python consumption.
|
|
70
|
-
|
|
71
|
-
This makes rpy-bridge suitable for production pipelines, CI, and bilingual
|
|
72
|
-
Python/R teams where R code must run reliably outside an interactive R session.
|
|
62
|
+
**rpy-bridge** is a Python-controlled **R execution orchestrator** (not a thin
|
|
63
|
+
rpy2 wrapper). It delivers deterministic, headless-safe R startup; project-root
|
|
64
|
+
inference; out-of-tree `renv` activation; isolated script namespaces; and robust
|
|
65
|
+
Python↔R conversions with dtype/NA normalization. Use it when you need
|
|
66
|
+
reproducible R execution from Python in production pipelines and CI.
|
|
73
67
|
|
|
74
68
|
**Latest release:** [`rpy-bridge` on PyPI](https://pypi.org/project/rpy-bridge/)
|
|
75
69
|
|
|
@@ -77,20 +71,40 @@ Python/R teams where R code must run reliably outside an interactive R session.
|
|
|
77
71
|
|
|
78
72
|
## What this is (and is not)
|
|
79
73
|
|
|
80
|
-
rpy-bridge **is not a thin rpy2 wrapper**.
|
|
74
|
+
rpy-bridge **is not a thin rpy2 wrapper**. Key differences:
|
|
81
75
|
|
|
82
|
-
|
|
83
|
-
-
|
|
84
|
-
-
|
|
85
|
-
-
|
|
86
|
-
-
|
|
76
|
+
- Infers R project roots via markers (`.git`, `.Rproj`, `renv.lock`, `DESCRIPTION`, `.here`)
|
|
77
|
+
- Activates `renv` even when it lives outside the calling directory
|
|
78
|
+
- Executes from the inferred project root so relative paths behave as R expects
|
|
79
|
+
- Runs headless by default (no GUI probing), isolates scripts from `globalenv()`
|
|
80
|
+
- Normalizes return values for Python (NAs, dtypes, data.frames) and offers comparison helpers
|
|
87
81
|
|
|
88
|
-
|
|
82
|
+
---
|
|
89
83
|
|
|
90
|
-
|
|
91
|
-
around execution context, filesystem behavior, and environment activation.
|
|
84
|
+
## Quickstart
|
|
92
85
|
|
|
93
|
-
|
|
86
|
+
Call a package function (no scripts):
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from rpy_bridge import RFunctionCaller
|
|
90
|
+
|
|
91
|
+
rfc = RFunctionCaller()
|
|
92
|
+
samples = rfc.call("stats::rnorm", 5, mean=0, sd=1)
|
|
93
|
+
median_val = rfc.call("stats::median", samples)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Call a function from a local script with `renv` (out-of-tree allowed):
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from pathlib import Path
|
|
100
|
+
from rpy_bridge import RFunctionCaller
|
|
101
|
+
|
|
102
|
+
project_dir = Path("/path/to/your-r-project")
|
|
103
|
+
script = project_dir / "scripts" / "example.R"
|
|
104
|
+
|
|
105
|
+
rfc = RFunctionCaller(path_to_renv=project_dir, scripts=script)
|
|
106
|
+
result = rfc.call("some_function", 42, named_arg="value")
|
|
107
|
+
```
|
|
94
108
|
|
|
95
109
|
## Core capabilities
|
|
96
110
|
|
|
@@ -161,7 +175,7 @@ reproducibility and avoid side effects during execution.
|
|
|
161
175
|
### Prerequisites
|
|
162
176
|
|
|
163
177
|
- System R installed and available on `PATH`
|
|
164
|
-
- Python 3.12
|
|
178
|
+
- Python 3.11+ (tested on 3.11–3.12)
|
|
165
179
|
|
|
166
180
|
### From PyPI
|
|
167
181
|
|
|
@@ -199,33 +213,7 @@ uv sync
|
|
|
199
213
|
|
|
200
214
|
## Usage
|
|
201
215
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
```python
|
|
205
|
-
from pathlib import Path
|
|
206
|
-
from rpy_bridge import RFunctionCaller
|
|
207
|
-
|
|
208
|
-
project_dir = Path("/path/to/your-r-project")
|
|
209
|
-
script = project_dir / "scripts" / "example.R"
|
|
210
|
-
|
|
211
|
-
caller = RFunctionCaller(
|
|
212
|
-
path_to_renv=project_dir,
|
|
213
|
-
script_path=script,
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
result = caller.call("some_function", 42, named_arg="value")
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
### Call base R functions (no local script)
|
|
220
|
-
|
|
221
|
-
```python
|
|
222
|
-
from rpy_bridge import RFunctionCaller
|
|
223
|
-
|
|
224
|
-
caller = RFunctionCaller(path_to_renv=None)
|
|
225
|
-
|
|
226
|
-
samples = caller.call("stats::rnorm", 10, mean=0, sd=1)
|
|
227
|
-
median_val = caller.call("stats::median", samples)
|
|
228
|
-
```
|
|
216
|
+
See Quickstart above and examples in `examples/basic_usage.py`.
|
|
229
217
|
|
|
230
218
|
---
|
|
231
219
|
|
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
# rpy-bridge
|
|
2
2
|
|
|
3
|
-
**rpy-bridge** is a Python-controlled **R execution orchestrator**
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
R code is executed when invoked from Python: project roots are inferred, `renv`
|
|
9
|
-
environments can be activated out-of-tree, relative paths behave as expected,
|
|
10
|
-
and return values are normalized for safe Python consumption.
|
|
11
|
-
|
|
12
|
-
This makes rpy-bridge suitable for production pipelines, CI, and bilingual
|
|
13
|
-
Python/R teams where R code must run reliably outside an interactive R session.
|
|
3
|
+
**rpy-bridge** is a Python-controlled **R execution orchestrator** (not a thin
|
|
4
|
+
rpy2 wrapper). It delivers deterministic, headless-safe R startup; project-root
|
|
5
|
+
inference; out-of-tree `renv` activation; isolated script namespaces; and robust
|
|
6
|
+
Python↔R conversions with dtype/NA normalization. Use it when you need
|
|
7
|
+
reproducible R execution from Python in production pipelines and CI.
|
|
14
8
|
|
|
15
9
|
**Latest release:** [`rpy-bridge` on PyPI](https://pypi.org/project/rpy-bridge/)
|
|
16
10
|
|
|
@@ -18,20 +12,40 @@ Python/R teams where R code must run reliably outside an interactive R session.
|
|
|
18
12
|
|
|
19
13
|
## What this is (and is not)
|
|
20
14
|
|
|
21
|
-
rpy-bridge **is not a thin rpy2 wrapper**.
|
|
15
|
+
rpy-bridge **is not a thin rpy2 wrapper**. Key differences:
|
|
22
16
|
|
|
23
|
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
17
|
+
- Infers R project roots via markers (`.git`, `.Rproj`, `renv.lock`, `DESCRIPTION`, `.here`)
|
|
18
|
+
- Activates `renv` even when it lives outside the calling directory
|
|
19
|
+
- Executes from the inferred project root so relative paths behave as R expects
|
|
20
|
+
- Runs headless by default (no GUI probing), isolates scripts from `globalenv()`
|
|
21
|
+
- Normalizes return values for Python (NAs, dtypes, data.frames) and offers comparison helpers
|
|
28
22
|
|
|
29
|
-
|
|
23
|
+
---
|
|
30
24
|
|
|
31
|
-
|
|
32
|
-
around execution context, filesystem behavior, and environment activation.
|
|
25
|
+
## Quickstart
|
|
33
26
|
|
|
34
|
-
|
|
27
|
+
Call a package function (no scripts):
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from rpy_bridge import RFunctionCaller
|
|
31
|
+
|
|
32
|
+
rfc = RFunctionCaller()
|
|
33
|
+
samples = rfc.call("stats::rnorm", 5, mean=0, sd=1)
|
|
34
|
+
median_val = rfc.call("stats::median", samples)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Call a function from a local script with `renv` (out-of-tree allowed):
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from pathlib import Path
|
|
41
|
+
from rpy_bridge import RFunctionCaller
|
|
42
|
+
|
|
43
|
+
project_dir = Path("/path/to/your-r-project")
|
|
44
|
+
script = project_dir / "scripts" / "example.R"
|
|
45
|
+
|
|
46
|
+
rfc = RFunctionCaller(path_to_renv=project_dir, scripts=script)
|
|
47
|
+
result = rfc.call("some_function", 42, named_arg="value")
|
|
48
|
+
```
|
|
35
49
|
|
|
36
50
|
## Core capabilities
|
|
37
51
|
|
|
@@ -102,7 +116,7 @@ reproducibility and avoid side effects during execution.
|
|
|
102
116
|
### Prerequisites
|
|
103
117
|
|
|
104
118
|
- System R installed and available on `PATH`
|
|
105
|
-
- Python 3.12
|
|
119
|
+
- Python 3.11+ (tested on 3.11–3.12)
|
|
106
120
|
|
|
107
121
|
### From PyPI
|
|
108
122
|
|
|
@@ -140,33 +154,7 @@ uv sync
|
|
|
140
154
|
|
|
141
155
|
## Usage
|
|
142
156
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
```python
|
|
146
|
-
from pathlib import Path
|
|
147
|
-
from rpy_bridge import RFunctionCaller
|
|
148
|
-
|
|
149
|
-
project_dir = Path("/path/to/your-r-project")
|
|
150
|
-
script = project_dir / "scripts" / "example.R"
|
|
151
|
-
|
|
152
|
-
caller = RFunctionCaller(
|
|
153
|
-
path_to_renv=project_dir,
|
|
154
|
-
script_path=script,
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
result = caller.call("some_function", 42, named_arg="value")
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
### Call base R functions (no local script)
|
|
161
|
-
|
|
162
|
-
```python
|
|
163
|
-
from rpy_bridge import RFunctionCaller
|
|
164
|
-
|
|
165
|
-
caller = RFunctionCaller(path_to_renv=None)
|
|
166
|
-
|
|
167
|
-
samples = caller.call("stats::rnorm", 10, mean=0, sd=1)
|
|
168
|
-
median_val = caller.call("stats::median", samples)
|
|
169
|
-
```
|
|
157
|
+
See Quickstart above and examples in `examples/basic_usage.py`.
|
|
170
158
|
|
|
171
159
|
---
|
|
172
160
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "rpy-bridge"
|
|
3
|
-
version = "0.5.
|
|
3
|
+
version = "0.5.2"
|
|
4
4
|
description = "Python-to-R interoperability engine with environment management, type-safe conversions, data normalization, and safe R function execution."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { file = "LICENSE" }
|
|
@@ -41,11 +41,6 @@ classifiers = [
|
|
|
41
41
|
|
|
42
42
|
]
|
|
43
43
|
|
|
44
|
-
[project.optional-dependencies]
|
|
45
|
-
r = ["rpy2>=3.5"]
|
|
46
|
-
dev = ["ipykernel>=7.1.0"]
|
|
47
|
-
docs = ["sphinx", "myst-parser"]
|
|
48
|
-
|
|
49
44
|
[project.urls]
|
|
50
45
|
Homepage = "https://github.com/vic-cheung/rpy-bridge"
|
|
51
46
|
"Issue Tracker" = "https://github.com/vic-cheung/rpy-bridge/issues"
|
|
@@ -57,6 +52,11 @@ build-backend = "setuptools.build_meta"
|
|
|
57
52
|
# -------------------------------
|
|
58
53
|
# uv dev dependencies
|
|
59
54
|
# -------------------------------
|
|
55
|
+
[project.optional-dependencies]
|
|
56
|
+
r = ["rpy2>=3.5"]
|
|
57
|
+
dev = ["ipykernel>=7.1.0"]
|
|
58
|
+
docs = ["sphinx", "myst-parser"]
|
|
59
|
+
|
|
60
60
|
[tool.uv]
|
|
61
61
|
dev-dependencies = [
|
|
62
62
|
"ruff>=0.6",
|
|
@@ -188,6 +188,16 @@ class RFunctionCaller:
|
|
|
188
188
|
except Exception:
|
|
189
189
|
pass
|
|
190
190
|
|
|
191
|
+
# Pass the current Python interpreter path to R so reticulate can use the same Python
|
|
192
|
+
# This is critical for nested Python→R→Python calls via reticulate
|
|
193
|
+
import sys
|
|
194
|
+
|
|
195
|
+
python_executable = sys.executable
|
|
196
|
+
venv_path = os.environ.get("VIRTUAL_ENV", "")
|
|
197
|
+
r(f'Sys.setenv(RPY_BRIDGE_PYTHON_EXECUTABLE = "{python_executable}")')
|
|
198
|
+
if venv_path:
|
|
199
|
+
r(f'Sys.setenv(VIRTUAL_ENV = "{venv_path}")')
|
|
200
|
+
|
|
191
201
|
self.ensure_r_package("withr")
|
|
192
202
|
|
|
193
203
|
if not hasattr(self, "_namespaces"):
|
|
@@ -357,9 +367,33 @@ class RFunctionCaller:
|
|
|
357
367
|
with localconverter(robjects.default_converter + pandas2ri.converter):
|
|
358
368
|
if is_na(obj):
|
|
359
369
|
return robjects.NULL
|
|
370
|
+
if isinstance(obj, pd.Timestamp):
|
|
371
|
+
# Convert single Timestamp to R Date (days since 1970-01-01)
|
|
372
|
+
days_since_epoch = (obj - pd.Timestamp("1970-01-01")).days
|
|
373
|
+
r = self.robjects.r
|
|
374
|
+
return r(f"as.Date({days_since_epoch}, origin='1970-01-01')")
|
|
360
375
|
if isinstance(obj, pd.DataFrame):
|
|
361
|
-
|
|
376
|
+
# Convert datetime columns to numeric (days since epoch) before R conversion
|
|
377
|
+
df_copy = obj.copy()
|
|
378
|
+
for col in df_copy.columns:
|
|
379
|
+
if pd.api.types.is_datetime64_any_dtype(df_copy[col]):
|
|
380
|
+
df_copy[col] = (df_copy[col] - pd.Timestamp("1970-01-01")).dt.days
|
|
381
|
+
r_df = pandas2ri.py2rpy(df_copy)
|
|
382
|
+
# Convert numeric date columns back to R Date class
|
|
383
|
+
r = self.robjects.r
|
|
384
|
+
for col in obj.columns:
|
|
385
|
+
if pd.api.types.is_datetime64_any_dtype(obj[col]):
|
|
386
|
+
r(f'class({r_df.rx2(col).r_repr()}) <- "Date"')
|
|
387
|
+
return r_df
|
|
362
388
|
if isinstance(obj, pd.Series):
|
|
389
|
+
if pd.api.types.is_datetime64_any_dtype(obj):
|
|
390
|
+
# Convert datetime Series to R Date vector
|
|
391
|
+
days = (obj - pd.Timestamp("1970-01-01")).dt.days.tolist()
|
|
392
|
+
r = self.robjects.r
|
|
393
|
+
days_vec = FloatVector(
|
|
394
|
+
[robjects.NA_Real if pd.isna(d) else float(d) for d in days]
|
|
395
|
+
)
|
|
396
|
+
return r("function(x) { class(x) <- 'Date'; x }")(days_vec)
|
|
363
397
|
return self._py2r(obj.tolist())
|
|
364
398
|
if isinstance(obj, (int, float, bool, str)):
|
|
365
399
|
return obj
|
|
@@ -4,6 +4,8 @@ DataFrame cleaning and post-processing utilities for R ↔ Python workflows.
|
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
+
import re
|
|
8
|
+
|
|
7
9
|
import numpy as np
|
|
8
10
|
import pandas as pd
|
|
9
11
|
|
|
@@ -34,6 +36,21 @@ def normalize_single_df_dtypes(df: pd.DataFrame) -> pd.DataFrame:
|
|
|
34
36
|
return df
|
|
35
37
|
|
|
36
38
|
|
|
39
|
+
# Token-based patterns to detect date/time columns; avoids substring matches like "endotoxin" or "runtime".
|
|
40
|
+
_DATE_TOKEN_RE = re.compile(
|
|
41
|
+
r"(?<![a-z0-9])"
|
|
42
|
+
r"(?:date|dt|time|dob|visit|start|end|created|updated|modified|timestamp|datetime|ts|dos)"
|
|
43
|
+
r"(?![a-z0-9])",
|
|
44
|
+
re.IGNORECASE,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _looks_like_date_column(col_name: str) -> bool:
|
|
49
|
+
"""Check if column name suggests it contains date values."""
|
|
50
|
+
col_lower = str(col_name).lower()
|
|
51
|
+
return bool(_DATE_TOKEN_RE.search(col_lower))
|
|
52
|
+
|
|
53
|
+
|
|
37
54
|
def fix_r_dataframe_types(df: pd.DataFrame) -> pd.DataFrame:
|
|
38
55
|
for col in df.columns:
|
|
39
56
|
series = df[col]
|
|
@@ -41,13 +58,18 @@ def fix_r_dataframe_types(df: pd.DataFrame) -> pd.DataFrame:
|
|
|
41
58
|
df[col] = series.mask(series == -2147483648, pd.NA)
|
|
42
59
|
if pd.api.types.is_numeric_dtype(series):
|
|
43
60
|
values = series.dropna()
|
|
44
|
-
|
|
61
|
+
# Only convert to date if values are in R's date range AND column name suggests a date
|
|
62
|
+
if (
|
|
63
|
+
not values.empty
|
|
64
|
+
and values.between(10000, 40000).all()
|
|
65
|
+
and _looks_like_date_column(col)
|
|
66
|
+
):
|
|
45
67
|
try:
|
|
46
68
|
df[col] = pd.to_datetime("1970-01-01") + pd.to_timedelta(series, unit="D")
|
|
47
69
|
except Exception:
|
|
48
70
|
pass
|
|
49
71
|
if pd.api.types.is_datetime64tz_dtype(series):
|
|
50
|
-
df[col] = series.dt.
|
|
72
|
+
df[col] = series.dt.tz_convert(None)
|
|
51
73
|
return df
|
|
52
74
|
|
|
53
75
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rpy-bridge
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.2
|
|
4
4
|
Summary: Python-to-R interoperability engine with environment management, type-safe conversions, data normalization, and safe R function execution.
|
|
5
5
|
Author-email: Victoria Cheung <victoriakcheung@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -59,17 +59,11 @@ Dynamic: license-file
|
|
|
59
59
|
|
|
60
60
|
# rpy-bridge
|
|
61
61
|
|
|
62
|
-
**rpy-bridge** is a Python-controlled **R execution orchestrator**
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
R code is executed when invoked from Python: project roots are inferred, `renv`
|
|
68
|
-
environments can be activated out-of-tree, relative paths behave as expected,
|
|
69
|
-
and return values are normalized for safe Python consumption.
|
|
70
|
-
|
|
71
|
-
This makes rpy-bridge suitable for production pipelines, CI, and bilingual
|
|
72
|
-
Python/R teams where R code must run reliably outside an interactive R session.
|
|
62
|
+
**rpy-bridge** is a Python-controlled **R execution orchestrator** (not a thin
|
|
63
|
+
rpy2 wrapper). It delivers deterministic, headless-safe R startup; project-root
|
|
64
|
+
inference; out-of-tree `renv` activation; isolated script namespaces; and robust
|
|
65
|
+
Python↔R conversions with dtype/NA normalization. Use it when you need
|
|
66
|
+
reproducible R execution from Python in production pipelines and CI.
|
|
73
67
|
|
|
74
68
|
**Latest release:** [`rpy-bridge` on PyPI](https://pypi.org/project/rpy-bridge/)
|
|
75
69
|
|
|
@@ -77,20 +71,40 @@ Python/R teams where R code must run reliably outside an interactive R session.
|
|
|
77
71
|
|
|
78
72
|
## What this is (and is not)
|
|
79
73
|
|
|
80
|
-
rpy-bridge **is not a thin rpy2 wrapper**.
|
|
74
|
+
rpy-bridge **is not a thin rpy2 wrapper**. Key differences:
|
|
81
75
|
|
|
82
|
-
|
|
83
|
-
-
|
|
84
|
-
-
|
|
85
|
-
-
|
|
86
|
-
-
|
|
76
|
+
- Infers R project roots via markers (`.git`, `.Rproj`, `renv.lock`, `DESCRIPTION`, `.here`)
|
|
77
|
+
- Activates `renv` even when it lives outside the calling directory
|
|
78
|
+
- Executes from the inferred project root so relative paths behave as R expects
|
|
79
|
+
- Runs headless by default (no GUI probing), isolates scripts from `globalenv()`
|
|
80
|
+
- Normalizes return values for Python (NAs, dtypes, data.frames) and offers comparison helpers
|
|
87
81
|
|
|
88
|
-
|
|
82
|
+
---
|
|
89
83
|
|
|
90
|
-
|
|
91
|
-
around execution context, filesystem behavior, and environment activation.
|
|
84
|
+
## Quickstart
|
|
92
85
|
|
|
93
|
-
|
|
86
|
+
Call a package function (no scripts):
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from rpy_bridge import RFunctionCaller
|
|
90
|
+
|
|
91
|
+
rfc = RFunctionCaller()
|
|
92
|
+
samples = rfc.call("stats::rnorm", 5, mean=0, sd=1)
|
|
93
|
+
median_val = rfc.call("stats::median", samples)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Call a function from a local script with `renv` (out-of-tree allowed):
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from pathlib import Path
|
|
100
|
+
from rpy_bridge import RFunctionCaller
|
|
101
|
+
|
|
102
|
+
project_dir = Path("/path/to/your-r-project")
|
|
103
|
+
script = project_dir / "scripts" / "example.R"
|
|
104
|
+
|
|
105
|
+
rfc = RFunctionCaller(path_to_renv=project_dir, scripts=script)
|
|
106
|
+
result = rfc.call("some_function", 42, named_arg="value")
|
|
107
|
+
```
|
|
94
108
|
|
|
95
109
|
## Core capabilities
|
|
96
110
|
|
|
@@ -161,7 +175,7 @@ reproducibility and avoid side effects during execution.
|
|
|
161
175
|
### Prerequisites
|
|
162
176
|
|
|
163
177
|
- System R installed and available on `PATH`
|
|
164
|
-
- Python 3.12
|
|
178
|
+
- Python 3.11+ (tested on 3.11–3.12)
|
|
165
179
|
|
|
166
180
|
### From PyPI
|
|
167
181
|
|
|
@@ -199,33 +213,7 @@ uv sync
|
|
|
199
213
|
|
|
200
214
|
## Usage
|
|
201
215
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
```python
|
|
205
|
-
from pathlib import Path
|
|
206
|
-
from rpy_bridge import RFunctionCaller
|
|
207
|
-
|
|
208
|
-
project_dir = Path("/path/to/your-r-project")
|
|
209
|
-
script = project_dir / "scripts" / "example.R"
|
|
210
|
-
|
|
211
|
-
caller = RFunctionCaller(
|
|
212
|
-
path_to_renv=project_dir,
|
|
213
|
-
script_path=script,
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
result = caller.call("some_function", 42, named_arg="value")
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
### Call base R functions (no local script)
|
|
220
|
-
|
|
221
|
-
```python
|
|
222
|
-
from rpy_bridge import RFunctionCaller
|
|
223
|
-
|
|
224
|
-
caller = RFunctionCaller(path_to_renv=None)
|
|
225
|
-
|
|
226
|
-
samples = caller.call("stats::rnorm", 10, mean=0, sd=1)
|
|
227
|
-
median_val = caller.call("stats::median", samples)
|
|
228
|
-
```
|
|
216
|
+
See Quickstart above and examples in `examples/basic_usage.py`.
|
|
229
217
|
|
|
230
218
|
---
|
|
231
219
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|