1from os.path import basename, dirname
2
3import base64
4import json
5import os
6from shutil import make_archive, unpack_archive
7from subprocess import run
8
9from omegaml.backends.basedata import BaseDataBackend
10from omegaml.backends.package.packager import RunnablePackageMixin
11from omegaml.runtimes.rsystem import rhelper
12from omegaml.util import tryOr
13
14
[docs]
15class RPackageData(RunnablePackageMixin, BaseDataBackend):
16 """
17 Backend to support R packages
18
19 Usage::
20
21 om.scripts.put('R://path/to/app.R', 'myname')
22 om.scripts.get('myname')
23
24 See Also:
25 * Rscript
26 * https://mastering-shiny.org/scaling-packaging.html (CC BY-NC-ND 4.0)
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).save()
64
[docs]
65 def get(self, name, localpath=None, keep=False, install=True, **kwargs):
66 """
67 Load package from store, install it locally and load.
68
69 This copies the package's .tar.gz file from om.scripts to a local temp
70 path and runs `pip install` on it.
71
72 :param name: the name of the package
73 :param keep: keep the packages load path in sys.path, defaults to False
74 :param localpath: the local path to store the package
75 :param install: if True call pip install on the retrieved package
76 :param kwargs:
77 :return: the loaded module (RScript)
78 """
79 pkgname = basename(name)
80 dstdir = localpath or self.data_store.tmppath
81 packagefname = '{}.tar.gz'.format(os.path.join(localpath or self.packages_path, pkgname))
82 os.makedirs(dirname(packagefname), exist_ok=True)
83 meta = self.data_store.metadata(name)
84 outf = meta.gridfile
85 with open(packagefname, 'wb') as pkgf:
86 pkgf.write(outf.read())
87 if install:
88 unpack_archive(packagefname, dstdir)
89 mod = RScript(dstdir)
90 else:
91 mod = os.path.join(dstdir, pkgname)
92 return mod
93
94 @property
95 def packages_path(self):
96 return os.path.join(self.data_store.tmppath, 'packages')
97
98
[docs]
99class RScript:
100 """ a Python proxy to the R process that runs a script
101
102 This provides the ``mod.run()`` interface for scripts so that
103 we can use the same semantics for R and python scripts.
104 """
105 def __init__(self, appdir):
106 self.appdir = appdir
107
[docs]
108 def run(self, om, **kwargs):
109 """ run the script in R session
110
111 Usage:
112 The script must exist as ``{self.appdir}/app.R``. It must implement
113 the ``omega_run()`` function.
114
115 Example::
116
117 # app.R
118 library(jsonlite)
119 omega_run <- function(om, kwargs) {
120 # if om was passed as "0", this means we're in a local mode, i.e. must import omegaml
121 om <- if (om == "0") import("omegaml") else om
122 s <- fromJSON(rawToChar(base64_dec(kwargs)))
123 s$message <- "hello from R"
124 return(toJSON(s))
125 }
126
127 Notes:
128 - If an R session is active, will run the script by calling the script's omega_run function
129 - If no R session is active, will use RScript to source the script and run omega_run function
130 - Expects the output to be in JSON format
131 """
132 r = rhelper()
133 if r is None:
134 r_kwargs = base64.b64encode(json.dumps(kwargs).encode('utf8')).decode('ascii')
135 rcmd = fr'Rscript -e source("{self.appdir}/app.R") -e omega_run(0,"{r_kwargs}")'
136 output = run(rcmd.split(' '), capture_output=True)
137 output = output.stdout
138 else:
139 r.source(f'{self.appdir}/app.R')
140 output = r.omega_run(om, kwargs)
141 return tryOr(lambda: json.loads(output), output)