Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: CI

on:
push:
branches: ["**"]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up uv (with Python 3.12)
uses: astral-sh/setup-uv@v3
with:
python-version: '3.12'
enable-cache: true

- name: Install package and test deps (uv)
run: |
uv venv
uv pip install -e .
uv pip install pytest
uv run pytest -q


148 changes: 148 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
pip-wheel-metadata/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
pytestdebug.log

# Translations
*.mo
*.pot

# Django stuff (uncomment if needed)
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
media/
staticfiles/

# Flask stuff
instance/
.webassets-cache

# Scrapy stuff
.scrapy

# Sphinx documentation
docs/_build/
doc/_build/

# PyBuilder
.pybuilder/
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# PEP 582; used by e.g. python-pdm
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
.env.*
.venv
.venv*/
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# pytype static type analyzer
.pytype/

# Cython debug symbols
cython_debug/

# OS files
.DS_Store
Thumbs.db

# Editors/IDE
.vscode/
.idea/

# Python version managers
.python-version

# Lock files (uncomment if you want to commit these instead)
poetry.lock
Pipfile.lock


out
36 changes: 35 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,35 @@
# mobuild
# mobuild

This package let's you construct Python packages from notebooks. You can add "## EXPORT" to cells that need to be moved and then use the utilities from this library to construct Python projects that you can install with uv.

> Install via `uv pip install mobuild` or run directly via `uvx mobuild`.

## Cli Features

### `export`

Turn a folder of Marimo notebooks into plain Python files in an output folder.

```bash
uvx mobuild export path/to/nbs path/to/output_src
```

### `init`

Create a new project from the bundled template.

```bash
uvx mobuild init my_project_name --output-folder .
```

## Python API

### `runtime_sync` from the notebook

Write the current marimo notebook to a Python file in an output folder.

```python
from mobuild import runtime_sync

runtime_sync("src/package_name")
```
10 changes: 10 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
install:
uv venv --allow-existing
source .venv/bin/activate
uv pip install -e . pytest marimo

test:
uv run pytest

clean:
rm .DS_Store
36 changes: 36 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
[project]
name = "mobuild"
version = "0.1.1"
description = "Add your description here"
readme = "README.md"
authors = [
{ name = "koaning", email = "vincentwarmerdam@gmail.com" }
]
requires-python = ">=3.12"
dependencies = ["typer", "marimo", "cookiecutter"]

[project.scripts]
mobuild = "mobuild:app"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build]
include = [
"src/mobuild/**"
]

[tool.hatch.build.targets.wheel]
packages = ["src/mobuild"]

[tool.hatch.build.targets.sdist]
include = [
"src/mobuild/**",
"README.md",
"LICENSE"
]

[tool.pytest.ini_options]
pythonpath = ["src"]
testpaths = ["tests"]
72 changes: 72 additions & 0 deletions src/mobuild/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from cookiecutter.main import cookiecutter
import typer
from marimo._ast.app import InternalApp
from pathlib import Path
import importlib
import importlib.util
import importlib.resources as resources
import hashlib
import sys

app = typer.Typer(no_args_is_help=True)

def _write_file(file: Path, out_folder: Path):
abs_path = str(Path(file).resolve())
mod_name = "mobuild_runtime_" + hashlib.sha1(abs_path.encode()).hexdigest()[:10]
spec = importlib.util.spec_from_file_location(mod_name, abs_path)
if spec is None or spec.loader is None:
raise ImportError(f"Could not load spec for {abs_path}")
module = importlib.util.module_from_spec(spec)
try:
sys.modules[mod_name] = module
spec.loader.exec_module(module)
finally:
# Ensure no global pollution
sys.modules.pop(mod_name, None)

app = getattr(module, "app")
app = InternalApp(app)
order = app.execution_order

codes = {k: v.code for k, v in app.graph.cells.items() if v.language=="python" and "## export" in v.code.lower()}
code_export = ""
for i in order:
if i in codes:
code_export += codes[i].replace("## Export", "") + "\n"
Path(out_folder / file.name).write_text(code_export)

@app.command(no_args_is_help=True)
def export(input_folder: Path, output_folder: Path):
"""Build a Python library from a folder of Marimo notebooks."""
if not Path(input_folder).exists():
typer.echo(f"Input folder {input_folder} does not exist")
raise typer.Exit(1)
files = Path(input_folder).glob("*.py")
out_folder = Path(output_folder)
out_folder.mkdir(parents=True, exist_ok=True)

for file in files:
try:
_write_file(file, out_folder)
except (ImportError, AttributeError) as e:
typer.echo(f"Error loading {file}: {e}")


@app.command(no_args_is_help=True)
def init(name: str, output_folder: Path = Path(".")):
"""Render a new project into the given output folder.

Expects the template assets to live under the installed package at
`mobuild/(cookiecutter/` with a `cookiecutter.json`.
"""
cookie_folder = Path(__file__).parent / "static" / "cookiecutter"
cookiecutter(
str(cookie_folder),
output_dir=str(output_folder),
extra_context={"project_name": name},
no_input=True
)

def runtime_sync(output_folder: Path):
"""Write the current marimo notebook to the output folder as a normal Python file."""
_write_file(__file__, output_folder)
6 changes: 6 additions & 0 deletions src/mobuild/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import typer
from mobuild import app

if __name__ == "__main__":
app()

Empty file added src/mobuild/py.typed
Empty file.
3 changes: 3 additions & 0 deletions src/mobuild/static/cookiecutter/cookiecutter.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"project_name": "my-project"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
install:
uv venv --allow-existing
source .venv/bin/activate
uv pip install -e . pytest marimo

test:
uv run pytest

clean:
rm -rf output/
rm -rf __pycache__/
rm -rf .pytest_cache/

build:
uv run mobuild export nbs src
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[project]
name = "{{ cookiecutter.project_name }}"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
authors = []
requires-python = ">=3.12"
dependencies = ["marimo"]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.pytest.ini_options]
pythonpath = ["src"]
testpaths = ["tests"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def test_very_basic():
assert 1 == 1
Loading
Loading