pycall.rb + pdm: call python from ruby - #60048
Conversation
…but HOME=/root and pdm tries to store config there which it cannot.
| environment env.merge(node['cdo-apps']['bundle_env'], {HOME: "/home/#{user}"}) | ||
| live_stream true | ||
| user user | ||
| group user |
There was a problem hiding this comment.
This chef recipe executes bundle exec rake dashboard:setup_db, which in turn loads pycall.rb, which does a pdm run which python to find the venv. This breaks before this change.
Why? despite running the execute with user user, chef does NOT load the user's env vars, so its like running with su blah rather than su - blah. PDM reads the HOME env var to decide where to store config, finds it cannot read /root/.config and bails out.
The fix? Just set HOME before shelling out to run bundle exec rake dashboard:setup_db
davidsbailey
left a comment
There was a problem hiding this comment.
just one comment after running through setup
…addsitedir() manually.
There was a problem hiding this comment.
our yarn.lock file lives in /apps. any reason pdm.lock shouldn't live inside /python?
There was a problem hiding this comment.
pdm.lock has to be parallel to pyproject.toml, and that in turn determines the root of our venv and thereby what paths pdm run works from. While I would have preferred to keep files out of the root dir, it seemed somehow clearer to match the semantics for bundle exec which is also whole repo. This links to python_venv.rb, which also depends on the "whole repo" thing. This could be altered, but this way was sort of the "natural" way.
Another thought is that (post-threading issues, I have a good start on those here btw: https://github.com/snickell/pycall_thread) you can use pyimport in Ruby paths, so its not just a /python thing.
| 1. Install pdm, which will be used later by `rake install` to install python | ||
| 1. `sudo pip3 install --prefix=/usr/local --upgrade pdm` | ||
| - alternatively, if you prefer pipx and have it configured path-wise: `pipx install pdm` |
There was a problem hiding this comment.
- Which ubuntu? Maybe I can replicate in docker
- Does
/usr/local/bin/pdmexist?
- If so, is
/usr/local/binin your path?- If so, maybe you just need a
zsh rehashto rescan the PATH?
- If so, maybe you just need a
- What about
/usr/local/lib/python3.8/dist-packages/pdm
- it might be other than python3.8 in the path, esp if not on ubuntu 20.04:
ls /usr/local/lib/
- If all else fails:
find /usr/local | grep pdm, on 20.04 clean docker mine produces:
circleci@75027a115ad9:/usr/local$ find /usr/local | grep pdm
/usr/local/lib/python3.8/dist-packages/pdm-2.17.3.dist-info
/usr/local/lib/python3.8/dist-packages/pdm-2.17.3.dist-info/WHEEL
/usr/local/lib/python3.8/dist-packages/pdm-2.17.3.dist-info/RECORD
/usr/local/lib/python3.8/dist-packages/pdm-2.17.3.dist-info/INSTALLER
/usr/local/lib/python3.8/dist-packages/pdm-2.17.3.dist-info/licenses
/usr/local/lib/python3.8/dist-packages/pdm-2.17.3.dist-info/licenses/LICENSE
/usr/local/lib/python3.8/dist-packages/pdm-2.17.3.dist-info/METADATA
/usr/local/lib/python3.8/dist-packages/pdm-2.17.3.dist-info/entry_points.txt
/usr/local/lib/python3.8/dist-packages/pdm
/usr/local/lib/python3.8/dist-packages/pdm/project
SNIP
/usr/local/lib/python3.8/dist-packages/pdm/resolver/core.py
/usr/local/lib/python3.8/dist-packages/pdm/resolver/__init__.py
/usr/local/lib/python3.8/dist-packages/pdm/resolver/providers.py
/usr/local/bin/pdm
There was a problem hiding this comment.
I ran into the same thing, and it was a path issue. Thanks Seth!
|
Docker tagged, .drone.yml update with new tag |

This PR is a continuation of @davidsbailey 's work in #60003 and allows Python code to be invoked easily from within Ruby. Ruby and Python have very similar object systems, and pycall.rb allows bridging the two. Invocations are very fast and can be generally treated as "function calls" in terms of performance.
pycall.rb permits a possible architecture where ruby and python code run in the same monolith. This should be considered an experiment for now, but if it proves reliable in practice, I would advocate this "side by side in the monolith" approach become the primary way we use python.
This PR uses pdm for python dependency management, virtualenv management, and python interpreter installation. pdm is similar to bundle + rbenv or yarn + nvm.
For more information on how we might structure python code in our repo, see: python/README.md
Example
Ruby code using the python
mathpackage:Ruby runs python code from /python/pycdo/pycdo/test_module/test_func.py:
See pycall_test.rb and python/README.md for more examples.
Motivation
Architectural advantages
As we've started to do AI work, we've discovered that python has more and better libraries of interest (compared to ruby). This has led to python being the obvious language for writing much of our AI code in, and we only expect the gap to grow over time.
As of 2024, we have decided not to use a microservice architecture, but in two cases pragmatism has necessitated auxiliary microservices. Both were needed for the same reason: support domain-specific must-use programming languages that would be too expensive to do in ruby. The javabuilder service was required in order to host
javac, and AI services like aiproxy have been needed to utilize python's ai-adjacent packages.Javabuilder is a small body of code, highly contained and not anticipated to grow, so it represents an exception rather than the rule. However, since we expect AI work to continue growing, and anticipate that we'll continue to see a gap between ruby and python in supporting AI libraries, I am concerned with a growing fork between "we write ruby here" and "we write python there".
With pycall.rb, we can keep our Majestic Monolith while easily taking advantage of amazing python libraries.
Everyday advantages
Directly calling python from ruby can make programming easier (vs multiple services):
scikit-learnmake a random graph easier to generate? Use it!Considerations
Performance
CPU
On my system, you can do 4 million function calls/second from ruby to python:
Conclusion: function calls from ruby => python are only 10x slower than ruby => ruby function calls, they can be treated essentially the same way.
Resident RAM
irbwithout our environmentps -o pid,rss=> irb process using42.6 MBRSSrequire ./lib/cdo/pycall.rbto load pycallps -o pid,rss=> irb process using49.7 MBRSSPyCall.import_module('math')to force use of pythonps -o pid,rss=> irb process using56.2 MBRSS56.2 - 42.6 => pycall + python take 13 MB of RAM (compare to python3 running by itself which takes 12.5MB of RAM).
Conclusion: we can expect each Ruby process to take a baseline extra 13 MB of RAM + ram taken by whatever python packages we want to use (e.g. with openai, numpy and boto3: 62MB)
Memory leaks
I tested on Linux with
MALLOC_CHECK_=1 irbas well as valgrind's memcheck, played with importing a couple dozen python modules and doing lots of function calls. Did not get report of any memory leaks or other memory issues.Thread Safety
We use threads in our puma configuration, and PyCall is not thread-safe. The good news is that its SO not thread safe that its very obvious if you make a mistake: it crashes instantly if ever used from a thread. I believe these issues should be addressed in a follow-up PR.
delayed_jobActiveJob workers in prod, which use processes instead of threads.bin/delayed_job startto use worker features. On the other hand, it'd be nice if our stock config worked with Python features without requiring a locals.yml tweak. Ideally, we could start delayed_jobs along with dashboard server. This is out of scope for this PR and would be an interesting subject for a future PR.pdm
pdm solves both the python interpreter version problem, the virtualenv problem, and the dependency versioning problem.
PDM:
bundle install=>pdm installpdm installwill install a python interpreter if needed to fulfill therequires-pythonclause in pyproject.tomlpdm installwill install packages into the venv, including deps of sub-packages like pycdobundle exec ____=>pdm run ____pdm addorpdm add --devcan be used to add deps and dev-deps to the pyproject.toml/python/subpackage/pyproject.tomlfiles rather than all mixed together in a top-level/pyproject.toml.pdm.lock, which allows thepyproject.tomldependencies to focus on what's actually compatible and not try to double as an imperfect lockfile (ala requirements.txt)Changes in PR
-/pyproject.toml also specifies a Python version: 3.12 so we start off as fresh as possible
cdo-pythonrake installandrake buildto runpdm install(equivalent tobundle install)/pyproject.toml/pythonshowing how to create sub-packages, and add dependencies to them.lint.rake(lint everything) andlint.rb(lint git commit hooks) to run linting on Python code in /python using RuffThings for a followup PR:
/python:/pythonand laddering up the resultsPost Merge: post on #developers with install instructions
Currently
brew install pdmsudo pip3 install --prefix=/usr/local --upgrade pdmpdm install --dev: should create a venv in .venvpdm run python3 --versionand confirm its 3.12.xTODO
awscli command works/python/README.mdcodedotorg/code-dot-org:pycall-pdm), probably will usecodedotorg/code-dot-org:1.10.0depending on when we mergeCo-authored-by: dave@code.org