One TOML file to rule them all
official ofiuco rules for multi-version multi-target Python setup
The term "ofiuco" refers to the constellation Ophiuchus, which is known as the "Serpent Bearer" in English.
The name origin is from the Greek ὀφιοῦχος with the Latinized form Ophiuchus /ˌɒfiˈjuːkəs/ which was further simplified to ofiuco /o.fiˈu.ko/ in some languages.
The brightest star is Rasalhague from the Arabic الحواء رأس
Ref in-the-sky.org
Motivation for a "yet another Python rules" was:
arm64
architecture with Python 3.10—3.12 runtimes and packaged as a Zip file which will be executed on Amazon Linux 2 or 2023
with some pre-installed packages like boto3
. The lambdas must be small and used as pre-filters for
SNS messages
via
AWS SQS aarch64
or Linux x86_64
pyproject.toml
file with tool sections [tool.poetry.*]
[[tool.poetry.source]]
name = "pytorch"
url = "https://download.pytorch.org/whl/cu121/"
priority = "explicit"
[tool.poetry.dependencies]
awslambdaric = "^2.0.4"
boto3 = "1.28.72"
botocore = "1.31.72"
...
safetensors = {version = "0.4.0", extras = ["numpy"]}
timm = "^0.9.10"
torch = [
{platform = "darwin", version = "2.2.1", source="pypi"},
{platform = "linux", url = "https://download.pytorch.org/whl/cu121/torch-2.2.1%2Bcu121-cp312-cp312-linux_x86_64.whl"},
]
torchinfo = "^1.8.0"
torchvision = [
{version = "0.17.1"},
{platform = "linux", version = "0.17.1", source="pytorch"},
]
Generate a lock file as poetry update
or using poetry_update
rule defined at @ofiuco//python:poetry.bzl
poetry.lock
file parsing to MODULE.bazel
aspoetry = use_extension("@ofiuco//python:extensions.bzl", "poetry")
poetry.parse(
name = "poetry",
lock = "@//tools/build_rules:poetry.lock",
)
use_repo(poetry, "poetry")
py_library(
name = "model",
srcs = [
"network.py",
],
data = [
"conf/model/default.yaml",
],
deps = [
"//src:torch",
"@poetry//:pydantic",
"@poetry//:timm",
],
)
load("@ofiuco//python:py_venv.bzl", "py_venv")
py_venv(
name = "torch",
visibility = ["//visibility:public"],
deps = [
"@poetry//:numpy",
"@poetry//:torch",
],
)
Let's define a platform in BUILD
file as
platform(
name = "aws_lambda",
constraint_values = [
"@platforms//os:linux",
"@platforms//cpu:arm64",
],
)
and Python lambda as
py_library(
name = "lambda",
srcs = ["lambda.py"],
deps = [
"//src/data/common:lambda_utils",
"@poetry//:awslambdaric",
"@poetry//:boto3",
"@poetry//:shapely",
],
)
Challenges:
lambda
is packed as a zip file for a fixed target platformawslambdaric
and boto3
packages shall not be included into zip packageshapely
package must have correct binary files
The rule load("@ofiuco//lib:py_zip.bzl", "py_zip")
has an attribute which is used as a transition
configuration for target
:
py_zip(
name = "deploy_aws_lambda",
platform = ":aws_lambda",
target = ":lambda",
)
Also possible to exclude files from the target zip by path patterns
py_zip(
...
exclude = [
"**.dist-info/**",
"**/__pycache__/**",
"**/bin/*",
# Ignore some packages since hey are provided by AWS Lambda runtime context.
"ofiuco**/awslambdaric**",
"ofiuco**/boto3**",
...
],
)
Define command_aws_batch
as
load("@rules_multirun//:defs.bzl", "command_with_transition", "multirun_with_transition")
def _aws_batch_platforms_impl(settings, attr):
return {"//command_line_option:platforms": [":aws_batch"]}
aws_batch_transition = transition(
implementation = _aws_batch_platforms_impl,
inputs = [],
outputs = ["//command_line_option:platforms"],
)
command_aws_batch = command_with_transition(aws_batch_transition)
Define :deploy
target as a push command with transition to aws_batch
oci_tarball(
name = "worker_tarball",
image = ":worker_image",
repo_tags = ["repo/project:latest"],
tags = ["manual"],
)
oci_push(
name = "worker_push",
image = ":worker_image",
remote_tags = ["latest"],
repository = ecr_registry.format(id = aws_account_id, region = aws_default_region,) + "/repo/project",
tags = ["manual"],
)
command_aws_batch(
name = "deploy",
arguments = [],
command = ":worker_push",
tags = ["manual"],
visibility = ["//visibility:public"],
)
BUILD
files is generated with declarationspackage(
name = "anyio",
constraint = "anyio==4.4.0",
description = "High level compatibility layer for multiple asynchronous event loop implementations",
files = {
"anyio-4.4.0-py3-none-any.whl": "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7",
"anyio-4.4.0.tar.gz": "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94",
},
deps = [":exceptiongroup", ":idna", ":sniffio", ":typing-extensions", "tomli", "pyreadline3", "colorama"],
markers = '''{"exceptiongroup":"python_version < \\\"3.11\\\"",
"typing-extensions":"python_version < \\\"3.11\\\"",
"tomli":"python_version < \\\"3.11\\\"",
"pyreadline3":"sys_platform == \\\"win32\\\" and python_version >= \\\"3.8\\\"",
"colorama":"platform_system == \\\"Windows\\\""}''',
visibility = ["//visibility:public"],
)
package
rule executed in a rule context with resolved toolchains
"@bazel_tools//tools/python:toolchain_type"
which is used to resolve markers and collect required dependencies"@bazel_tools//tools/cpp:toolchain_type"
which is used to compile wheels if no binary files providedexec
configuration Python toolchain to run pip install command with listed files and corresponding SHA256 checksI welcome any feedback on the rules and would be glad to make them "official", [sic].