> ## Documentation Index
> Fetch the complete documentation index at: https://site.aspect.build/llms.txt
> Use this file to discover all available pages before exploring further.

# Fetching ML models under Bazel

> Learn how to use Bazel to fetch and cache NLTK data hermetically, ensuring reproducible builds and reliable Python tests without network dependencies.

export const BlogPost = ({title, date, authors, tags, image, children}) => {
  const tagList = tags ? tags.split(", ").filter(Boolean) : [];
  const tagSlug = t => t.toLowerCase().replace(/&/g, "").replace(/\+/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
  const formattedDate = date ? new Date(date + "T00:00:00").toLocaleDateString("en-US", {
    year: "numeric",
    month: "long",
    day: "numeric"
  }) : "";
  return <section className="w-full flex justify-center px-4 py-12 md:py-16">
      <div style={{
    maxWidth: "800px",
    width: "100%"
  }}>
        {image && (typeof image === "string" ? <img noZoom src={image} alt={title} className="w-full rounded-xl mb-8" style={{
    maxHeight: "400px",
    objectFit: "cover"
  }} /> : <div className="blog-post-hero-image">{image}</div>)}
        <h1 className="text-3xl md:text-4xl font-bold text-zinc-900 dark:text-white">
          {title}
        </h1>
        <div className="flex flex-wrap items-center gap-3 mt-4 text-sm text-zinc-500 dark:text-zinc-400">
          {authors && <span>{authors}</span>}
          {authors && formattedDate && <span>·</span>}
          {formattedDate && <span>{formattedDate}</span>}
        </div>
        {tagList.length > 0 && <div className="flex flex-wrap gap-2 mt-3">
            {tagList.map(tag => <a key={tag} href={"/blog/tags/" + tagSlug(tag)} className="px-2 py-0.5 rounded-full text-xs bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-400 hover:bg-blue-100 dark:hover:bg-blue-900/40 hover:text-blue-700 dark:hover:text-blue-300 transition">
                {tag}
              </a>)}
          </div>}
        <hr className="my-8 border-zinc-200 dark:border-zinc-700" />
        <div className="prose dark:prose-invert max-w-none">{children}</div>
      </div>
    </section>;
};

export const MarketingPage = () => <div className="marketing-page-marker" style={{
  display: "none"
}} />;

export const Section = ({children, className = "", gray = false, dark = false, id}) => <section id={id} className={`w-full flex justify-center px-4 py-16 md:py-24 ${gray ? "bg-gray-50 dark:bg-zinc-900" : dark ? "bg-zinc-900 dark:bg-zinc-950" : ""} ${className}`}>
    <div className="w-full" style={{
  maxWidth: "1140px"
}}>
      {children}
    </div>
  </section>;

<MarketingPage />

<BlogPost title="Fetching ML models under Bazel" date="2024-03-03" authors="Alex Eagle" tags="Python, Bazel">
  <img noZoom src="https://mintcdn.com/aspectbuild/x1L7Iep716jCyJVo/images/blog/hashnode/hn-237621392e.jpeg?fit=max&auto=format&n=x1L7Iep716jCyJVo&q=85&s=905a79fb89e35750383eb1d02e2e5520" alt="" className="blog-post-cover" width="1254" height="836" data-path="images/blog/hashnode/hn-237621392e.jpeg" />

  This applies the same lazy-fetch technique as [Avoiding eager fetches](/blog/avoid-eager-fetches) — separating an external tool's *fetch* step from its *build* step — but this time to a Python package used for Machine Learning tasks, NLTK Data [https://www.nltk.org/data.html](https://www.nltk.org/data.html)

  Like the previous article, we can read the install documentation provided by the tool we want to run as a Bazel action. When we do, we find they suggest a non-hermetic "installation" path that creates a `/usr/local/share/nltk_data` folder on your machine. Here's the error printed when the data isn't found:

  ```plaintext theme={null}
  Resource punkt not found.
  Please use the NLTK Downloader to obtain the resource:
  >>> import nltk
  >>> nltk.download('punkt')
  For more information see: https://www.nltk.org/data.html
  ```

  We don't want to follow this guidance. It also forces us to add `tags = ["requires-network"]` on a target, which is a smell of something wrong. The installed state is unmanaged by the build system, and won't exist on the CI machine where your tests run. You could prepare the CI machines in the same way of course, but this isn't what we want under Bazel. It makes the build non-reproducible since it depends on machine state, and not portable to other executors.

  Instead, we'll use the Bazel Downloader to prepare the package. Like before, the key observation is that these "lazy fetching" patterns always have a cache or install folder layout the tool expects to read. So long as we can construct a cache folder that meets all the assumptions of the tool, we can just stitch our result folder into the tool's runtime using the affordance provided, in this case the `NLTK_DATA` environment variable.

  ## Fetching NLTK data

  For the sake of example, let's say our NLTK usage assumes that the Punkt tokenizer is "installed". We can see that it's distributed in their GitHub repo: [https://github.com/nltk/nltk\_data/tree/gh-pages/packages/tokenizers](https://github.com/nltk/nltk_data/tree/gh-pages/packages/tokenizers)

  Our first step is to fetch the data. We want to use the Bazel Downloader, for reasons described in my earlier post: [https://blog.aspect.dev/configuring-bazels-downloader](https://blog.aspect.dev/configuring-bazels-downloader). That can use the `repository_ctx.download*` functions, but this case is simple, so we can just use the `http_archive` helper from our `WORKSPACE` or `MODULE.bazel` file:

  ```python theme={null}
      # Bazel download from https://www.nltk.org/nltk_data/ rather than follow their lame instructions
      http_archive(
          name = "nltk_data_punkt",
          build_file_content = """exports_files(["punkt"], visibility = ["//visibility:public"])""",
          sha256 = "51c3078994aeaf650bfc8e028be4fb42b4a0d177d41c012b6a983979653660ec",
          # note: 'gh-pages' branch replaced by a commit hash for determinism
          urls = ["https://raw.githubusercontent.com/nltk/nltk_data/1d3c34b4cfd6059986bf4bc604e5929335ab92ff/packages/tokenizers/punkt.zip"],
      )
  ```

  Now, we need to prepare a folder that mimics the cache folder structure nltk's downloader creates. It's sufficient to do a single `copy_to_directory` action. To make it look pretty in a Bazel context, we'll wrap it with a trivial Macro that adds some documentation. Let's put this content in `nltk.bzl`:

  ```python theme={null}
  "Helpers for https://www.nltk.org/"

  load("@aspect_bazel_lib//lib:copy_to_directory.bzl", "copy_to_directory")

  def nltk_data(name, corpora):
      """Assemble a folder following instructions at https://www.nltk.org/data.html#manual-installation
      Args:
          name: name of resulting target
          corpora: list of packages from https://github.com/nltk/nltk_data/tree/gh-pages/packages
              e.g. ["tokenizers/punkt", "sentiment/vader_lexicon"]
              Note that these need to be fetched by Bazel, see /tools/bazel/fetch.bzl to add more.
      """

      copy_to_directory(
          name = name,
          # Prohibit this data being used in production,
          # which would pose a vulnerability issue and could cause outages.
          testonly = True,
          # convention for external repos is using the last segment of the name, e.g.
          # tokenizers/punkt is in @nltk_data_punkt
          srcs = ["@nltk_data_{0}//:{0}".format(each.split("/")[-1]) for each in corpora],
          include_external_repositories = ["nltk_data_*"],
          replace_prefixes = {each.split("/")[-1]: each for each in corpora},
      )
  ```

  ## Using in a BUILD target

  Now we'll edit our Python target to be able to resolve the data. This is likely a `py_binary` or `py_test` target. First, load the `nltk.bzl` file and call the macro, for example:

  ```python theme={null}
  nltk_data(
      name = "nltk_data",
      corpora = ["tokenizers/punkt"],
  )
  ```

  This `:nltk_data` target looks like an nltk-installed data folder.

  So in our Python target, we just need to set an environment variable pointing to it:

  ```python theme={null}
  py_test(
      ...
      data = [
          ":nltk_data",
          "//path/to/tests/words",
      ],
      env = {"NLTK_DATA": "$(rootpath :nltk_data)"},
  )
  ```

  Now the data is downloaded by Bazel and then hermetically provided to the test action at runtime with no network access.
</BlogPost>
