Self-hosted service
fmind/mlops-python-package avatar
fmind/mlops-python-package

MLOps Python Package: the template you clone installs as bikes

A comprehensive Python package template to kickstart and standardize your MLOps initiatives and data pipelines.

1,420 stars200 forksJupyter NotebookMIT

At a glance

What is it?
fmind/mlops-python-package is a cookiecutter style MLOps template, and the thing it actually builds is a package called bikes that predicts how many bikes are available. Its compose file also runs an MLflow server older than the client library it requires.
Who is it for?
Treat this as a starting skeleton rather than a library to depend on. Three things need checking before you adopt it: whether the distribution name bikes and the version 6.0.1 you inherit are acceptable in your own packaging, whether the MLflow client floor of 3.16.1 can talk to the 3.15.1 server the compose file starts, and whether the 3.14 only requirement matches every machine that will build the image.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 12 days ago.
What is it written in?
Mainly Jupyter Notebook, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 2, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The distribution is called bikes, not mlops-python-package

The repository is an MLOps template, and the package it builds is a bike counting example.

toml
[project]
name = "bikes"
version = "6.0.1"
description = "Predict the number of bikes available."
requires-python = ">=3.14"

So the wheel you produce is bikes, the command it installs is named after that project rather than after the template, and the hosted API documentation path ends in bikes.html. Every consumer of the project metadata sees the example identity, not the template identity: the keywords list is mlops, python, package, the homepage and repository URLs point back at the GitHub repository, and the description line describes bike availability. The install steps say only to adapt the code base to your desire, and the version field carries the template's own numbering, so a fresh clone starts at 6.0.1 against the newest listed tag v6.0.1 dated 2026-10-03. That numbering comes from the template's iteration count, following v5.0.0 in July and v6.0.0 in August 2026, not from any semantic versioning of the bike predictor. Four related projects are linked as places to go next rather than folded in: a cookiecutter template for building and deploying Python packages and Docker images, an LLMOps flavoured example package, a course repository and a set of agent skills. A .cruft.json file at the root is the record cookiecutter writes for a generated project, so the lineage is explicit: this is generated output, and the layout invites regenerating it from the parent template.

compose runs MLflow 3.15.1 while the client asks for 3.16.1

The local tracking server and the client library that talks to it are pinned to different releases of the same project.

yaml
    image: ghcr.io/mlflow/mlflow:v3.15.1
    ports:
      - 5000:5000

The dependency list requires mlflow>=3.16.1, an open floor that a fresh resolve will push higher still, while the compose service runs a pinned server image at 3.15.1. Any command that runs the stack as shipped therefore pairs a client at or above 3.16.1 with a server one minor release behind it, and nothing in either file notes the gap or explains which side should move. The compose file does document its own storage decision, in a comment attached to the command: the SQLAlchemy backend is the same store the package writes to locally, because the file store is in maintenance mode in MLflow 3 and was never designed for the model registry. Both stores are therefore SQLite files, bind mounted from the repository root as ./mlflow.db and ./mlruns, so runs and registered models survive a container rebuild and are shared with whoever has the directory.

The tracking server binds all interfaces with no authentication in the defaults

The compose service publishes a port and starts the server on every interface.

yaml
    command: mlflow server --host 0.0.0.0 --port 5000 --backend-store-uri sqlite:///mlflow.db --artifacts-destination ./mlruns

The port mapping 5000:5000 together with --host 0.0.0.0 means the tracking UI, the registry and the artifact root are reachable from anywhere that can route to the host, and neither the compose file nor the example environment file configures a credential, a proxy or a TLS setting. The defaults in .env.example point the client at the local file instead.

bash
MLFLOW_TRACKING_URI=sqlite:///mlflow.db
MLFLOW_REGISTRY_URI=sqlite:///mlflow.db

That file's own comment suggests the next step for a team, pointing both variables at a real database such as PostgreSQL or MySQL, or at a tracking server at http://127.0.0.1:5000, and states that the client code does not change when you do. That advice covers storage and sharing, not access control. Anyone who replaces the SQLite path with a server URL inherits the same open bind, so the authentication question stays open unless it is answered separately.

Permissive floors on purpose, with uv.lock holding the real pins

The dependency list explains itself about ABI coupling, which is unusual and worth reading before pinning anything.

toml
  # numba/numpy/pyarrow/shap are ABI-coupled: keep permissive floors so uv's universal
  # (all-platform) resolution stays satisfiable; the lockfile pins the latest tested versions.

Four packages share compiled extension boundaries, so their floors are held open on purpose to keep a cross platform resolve satisfiable, and uv.lock is where the tested versions actually live. That is why the floors look so high for a template, with mlflow at 3.16.1, pyarrow at 19.0.1, numpy at 2.1.3, scikit-learn at 1.9.0 and setuptools at 83.0.0. The consequence for anyone who copies the project is that the floors are not the constraint and the lockfile is, so a careless upgrade path is editing pyproject and expecting the same versions. There is a second lock in the tree, mise.lock, next to mise.toml, and the development group carries the same kind of comment about a formatter version that must not be downgraded, since a different release would disagree with the repository's formatting.

Python 3.14 is required in three places, with no lower bound option

The floor is not a preference anywhere in this template.

toml
requires-python = ">=3.14"

The prerequisites section asks for Python>=3.14 and links to what is new in 3.14 for the language features and performance, the dependency metadata repeats the same floor so an installer refuses anything older, and both Docker stages build FROM python:3.14-slim. The other two prerequisites are uv>=0.12, which initializes the project virtual environment and its dependencies, and mise, which runs the canonical task list: install, format, check, test, build and all. Those tasks are described as shared by the git hooks and CI, which is why lefthook.yml and mise.toml both appear at the top level. A .python-version file pins the interpreter for tools that read it. There is no documented way to run the template on 3.12 or 3.13, and nothing states which of the dependencies actually needs 3.14, so downgrading means working out the reason yourself.

The tools list names hadolint, the tree ships a Trivy configuration

The table of contents is a hundred lines of anchors, and checking it against the tree turns up one mismatch and some duplication.

- [Image Linting: hadolint](#image-linting-hadolint) - [Typing: ty](#typing-ty) - [Monitoring : Mlflow Evaluate](#monitoring--mlflow-evaluate)

The image linting entry names hadolint, and the top level of the repository has no hadolint configuration; what it does have is trivy.yaml, a scanner configuration for a different tool. Under Code, three separate subsections are devoted to Ruff, one each for formatting, quality and security, and a fourth tool, ty, is assigned the typing slot. The Tools list also has two subsections both titled Automation, one under Usage and one under Tools, which is why a table of contents with duplicate labels exists at all. Beyond the tool table, the root carries AGENTS.md, a .gemini directory, cliff.toml for the changelog generator, dprint.jsonc for non Python formatting, a VS Code workspace file, and an MLproject file, so the documentation surface and the tooling surface are both maintained in the same directory. The compose file defines exactly one service, the MLflow server, so anything else you add is expected to run outside the container, and the primary language recorded for the repository is Jupyter Notebook, with notebooks/, images/, data/ and outputs/ sitting beside src/ and tests/.

The visible configuration paragraph ends one word into a sentence

The usage section arrives and stops.

You can add or edit config files in the `confs/` folder to chang

That is the whole configuration instruction on the page, cut off in the middle of the word change, so the config file names, their keys and their defaults are not stated anywhere in the front page. A confs/ directory does exist at the top level, alongside data/, images/, notebooks/ and outputs/, and the tool table names OmegaConf as the parser, Pydantic as the validator, YAML as the format and Cloudpathlib as the reader, which is the stack a config layer in this template would be built on. The one piece of configuration that is spelled out sits in the environment file instead.

bash
MLFLOW_TRACKING_URI=sqlite:///mlflow.db
MLFLOW_REGISTRY_URI=sqlite:///mlflow.db

The comment above those two lines says mise.toml sources that file, so every task and every hook sees the values. So a new user inherits a config directory whose contents are undocumented, a task runner that loads environment variables for them, and a four tool configuration stack to guess from.

The image entry point is bikes, and the default command prints help

The container is built to run one console script as an unprivileged user.

dockerfile
USER 10001:10001
COPY --from=build --chown=10001:10001 /app/.venv /app/.venv
ENV PATH="/app/.venv/bin:$PATH"
ENTRYPOINT ["bikes"]
CMD ["--help"]

The build stage copies a pinned uv binary from a specific version rather than a floating tag, which the comment explains is so that the dependency scanner can see the image ecosystem, then syncs the environment twice from the lockfile: once without the project and without development dependencies, then again non editable with the copied source. The final stage creates a fixed numeric group and user, both 10001, with the stated reasons of stable file ownership across rebuilds and bind mounts and resolvability on a host that does not share the image's password file. Running the image with no arguments prints help, so the artifact is a packaged CLI rather than a service, and the MLflow server lives in the compose file beside it rather than inside it.

Editorial conclusion

Treat this as a starting skeleton rather than a library to depend on. Three things need checking before you adopt it: whether the distribution name bikes and the version 6.0.1 you inherit are acceptable in your own packaging, whether the MLflow client floor of 3.16.1 can talk to the 3.15.1 server the compose file starts, and whether the 3.14 only requirement matches every machine that will build the image. If you expose the tracking server beyond localhost, add authentication and a real database, because the default is a single SQLite file bind mounted from the repository root, with the port published and the server bound to all interfaces.

Frequently asked questions

What package name does fmind/mlops-python-package install?

bikes. The project table sets the name to bikes with the description Predict the number of bikes available., the console script is bikes = 'bikes.scripts:main', the Dockerfile uses the same name as its entry point, and the hosted documentation path ends in bikes.html. The template's own identity only appears in the repository and homepage URLs.

Which Python version does fmind/mlops-python-package support?

Only 3.14 and newer. requires-python is >=3.14, the prerequisites ask for Python>=3.14, and both Docker stages build from python:3.14-slim. There is no documented lower bound option, and the page does not say which dependency forces it.

Which MLflow version does fmind/mlops-python-package run?

Two different ones. The dependency asks for mlflow>=3.16.1, while docker-compose.yml starts the server image ghcr.io/mlflow/mlflow:v3.15.1. Both tracking and registry default to a SQLite file at sqlite:///mlflow.db, with artifacts under ./mlruns and the server bound to 0.0.0.0 on port 5000.

How do you install fmind/mlops-python-package?

Clone the repository, then run mise run install from the project root. The prerequisites are Python 3.14, uv 0.12 or newer, and mise, and the canonical mise tasks are install, format, check, test, build and all, which the git hooks and CI share.

Can fmind/mlops-python-package target Databricks or AWS?

The next steps section names Databricks and AWS as example compute platforms and model registries, and leaves adapting the code to the target solution to you. The environment file also says that pointing MLFLOW_TRACKING_URI and MLFLOW_REGISTRY_URI at PostgreSQL, MySQL or a tracking server does not require changing the client code.

Official sources

  1. fmind/mlops-python-package on GitHub
  2. License: MIT
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/fmind-mlops-python-package.svg)](https://hysenlabs.com/projects/fmind-mlops-python-package)