1import base64
2import json
3import os
4from os.path import basename, dirname
5from shutil import make_archive, unpack_archive
6from subprocess import run
7
8from omegaml.backends.basedata import BaseDataBackend
9from omegaml.backends.package.packager import RunnablePackageMixin
10from omegaml.runtimes.rsystem import rhelper
11from omegaml.util import tryOr
12
13
[docs]
14class RPackageData(RunnablePackageMixin, BaseDataBackend):
15 """
16 Backend to support R packages
17
18 Usage::
19
20 om.scripts.put('R://path/to/app.R', 'myname')
21 om.scripts.get('myname')
22
23 See Also:
24 * Rscript
25 * https://mastering-shiny.org/scaling-packaging.html (CC BY-NC-ND 4.0)
26 """
27
28 KIND = 'package.r'
29
[docs]
30 @classmethod
31 def supports(self, obj, name, **kwargs):
32 return isinstance(obj, str) and obj.startswith('R://')
33
[docs]
34 def put(self, obj, name, attributes=None, **kwargs):
35 """
36 save a R package
37
38 This takes the full path to a setuptools setup.py, or a directory
39 containing a setup.py file. It then executes `python setup.py sdist`
40 and stores the resulting .tar.gz file in om.scripts
41
42 :param obj: full path to package file or directory, syntax as
43 pkg://path/to/dist.tar.gz or pkg://path/to/setup.py
44 :param name: name of package. must be the actual name of the package
45 as specified in setup.py
46 :return: the Metadata object
47 """
48 pkgsrc = pkgdist = obj.split('//')[1]
49 pkgsrc = pkgsrc.replace('app.R', '')
50 if not 'tar.gz' in os.path.basename(pkgdist):
51 distdir = os.path.join(pkgsrc, 'dist')
52 os.makedirs(distdir, exist_ok=True)
53 base_name = os.path.join(distdir, f'{name}')
54 pkgdist = make_archive(base_name, 'gztar', pkgsrc)
55 filename = self.data_store.object_store_key(name, 'pkg', hashed=True)
56 gridfile = self._store_to_file(self.data_store, pkgdist, filename)
57 return self.data_store._make_metadata(
58 name=name,
59 prefix=self.data_store.prefix,
60 bucket=self.data_store.bucket,
61 kind=RPackageData.KIND,
62 attributes=attributes,
63 gridfile=gridfile,
64 ).save()
65
[docs]
66 def get(self, name, localpath=None, keep=False, install=True, **kwargs):
67 """
68 Load package from store, install it locally and load.
69
70 This copies the package's .tar.gz file from om.scripts to a local temp
71 path and runs `pip install` on it.
72
73 :param name: the name of the package
74 :param keep: keep the packages load path in sys.path, defaults to False
75 :param localpath: the local path to store the package
76 :param install: if True call pip install on the retrieved package
77 :param kwargs:
78 :return: the loaded module (RScript)
79 """
80 pkgname = basename(name)
81 dstdir = localpath or self.data_store.tmppath
82 packagefname = f'{os.path.join(localpath or self.packages_path, pkgname)}.tar.gz'
83 os.makedirs(dirname(packagefname), exist_ok=True)
84 meta = self.data_store.metadata(name)
85 outf = meta.gridfile
86 with open(packagefname, 'wb') as pkgf:
87 pkgf.write(outf.read())
88 if install:
89 unpack_archive(packagefname, dstdir)
90 mod = RScript(dstdir)
91 else:
92 mod = os.path.join(dstdir, pkgname)
93 return mod
94
95 @property
96 def packages_path(self):
97 return os.path.join(self.data_store.tmppath, 'packages')
98
99
[docs]
100class RScript:
101 """a Python proxy to the R process that runs a script
102
103 This provides the ``mod.run()`` interface for scripts so that
104 we can use the same semantics for R and python scripts.
105 """
106
107 def __init__(self, appdir):
108 self.appdir = appdir
109
[docs]
110 def run(self, om, **kwargs):
111 """run the script in R session
112
113 Usage:
114 The script must exist as ``{self.appdir}/app.R``. It must implement
115 the ``omega_run()`` function.
116
117 Example::
118
119 # app.R
120 library(jsonlite)
121 omega_run <- function(om, kwargs) {
122 # if om was passed as "0", this means we're in a local mode, i.e. must import omegaml
123 om <- if (om == "0") import("omegaml") else om
124 s <- fromJSON(rawToChar(base64_dec(kwargs)))
125 s$message <- "hello from R"
126 return(toJSON(s))
127 }
128
129 Notes:
130 - If an R session is active, will run the script by calling the script's omega_run function
131 - If no R session is active, will use RScript to source the script and run omega_run function
132 - Expects the output to be in JSON format
133 """
134 r = rhelper()
135 if r is None:
136 r_kwargs = base64.b64encode(json.dumps(kwargs).encode('utf8')).decode('ascii')
137 rcmd = rf'Rscript -e source("{self.appdir}/app.R") -e omega_run(0,"{r_kwargs}")'
138 output = run(rcmd.split(' '), capture_output=True)
139 output = output.stdout
140 else:
141 r.source(f'{self.appdir}/app.R')
142 output = r.omega_run(om, kwargs)
143 return tryOr(lambda: json.loads(output), output)