agentbuild 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,22 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ python-version: ["3.11", "3.13"]
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: astral-sh/setup-uv@v6
17
+ with:
18
+ python-version: ${{ matrix.python-version }}
19
+ - run: uv sync --group dev
20
+ - run: uv run ruff check .
21
+ - run: uv run ruff format --check .
22
+ - run: uv run pytest
@@ -0,0 +1,26 @@
1
+ # Byte-compiled / cache
2
+ __pycache__/
3
+ *.py[cod]
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ .mypy_cache/
7
+
8
+ # Build / packaging
9
+ build/
10
+ dist/
11
+ *.egg-info/
12
+
13
+ # Environments
14
+ .venv/
15
+ venv/
16
+ .env
17
+
18
+ # Coverage
19
+ .coverage
20
+ htmlcov/
21
+ coverage.xml
22
+
23
+ # Editors / OS
24
+ .idea/
25
+ .vscode/
26
+ .DS_Store
@@ -0,0 +1,17 @@
1
+ repos:
2
+ - repo: https://github.com/pre-commit/pre-commit-hooks
3
+ rev: v6.0.0
4
+ hooks:
5
+ - id: trailing-whitespace
6
+ - id: end-of-file-fixer
7
+ - id: check-yaml
8
+ - id: check-toml
9
+ - id: check-added-large-files
10
+ - id: check-merge-conflict
11
+
12
+ - repo: https://github.com/astral-sh/ruff-pre-commit
13
+ rev: v0.17.1
14
+ hooks:
15
+ - id: ruff-check
16
+ args: [--fix]
17
+ - id: ruff-format
@@ -0,0 +1,57 @@
1
+ .DEFAULT_GOAL := help
2
+
3
+ # Python versions for `make test-matrix`. Override with: make test-matrix PYTHONS="3.11 3.12"
4
+ PYTHONS ?= 3.11 3.12 3.13 3.14 3.15
5
+
6
+ # Use the activated virtualenv if there is one, otherwise uv's project .venv.
7
+ UV_ENV := $(if $(VIRTUAL_ENV),--active)
8
+ RUN := uv run $(UV_ENV)
9
+
10
+ .PHONY: help install lint format test cov test-matrix pre-commit check build publish upgrade clean
11
+
12
+ help: ## Show available commands
13
+ @awk 'BEGIN {FS = ":.*## "} /^[a-zA-Z_-]+:.*## / {printf " \033[36m%-12s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
14
+
15
+ install: ## Install the package and dev tools, and set up git hooks
16
+ uv sync $(UV_ENV)
17
+ $(RUN) pre-commit install
18
+
19
+ lint: ## Lint with ruff and check formatting
20
+ $(RUN) ruff check .
21
+ $(RUN) ruff format --check .
22
+
23
+ format: ## Auto-fix lint issues and format code
24
+ $(RUN) ruff check --fix .
25
+ $(RUN) ruff format .
26
+
27
+ test: ## Run tests on the current Python
28
+ $(RUN) pytest
29
+
30
+ cov: ## Run tests with a coverage report
31
+ $(RUN) pytest --cov=agentbuild --cov-report=term-missing
32
+
33
+ test-matrix: ## Run tests on every Python version in PYTHONS
34
+ @for v in $(PYTHONS); do \
35
+ echo "==> Python $$v"; \
36
+ uv run --isolated --python $$v pytest -q || exit 1; \
37
+ done
38
+
39
+ pre-commit: ## Run all pre-commit hooks on all files
40
+ $(RUN) pre-commit run --all-files
41
+
42
+ check: lint test ## Run the same checks as CI
43
+
44
+ build: ## Build the sdist and wheel into dist/
45
+ rm -rf dist
46
+ uv build
47
+
48
+ publish: build ## Build and upload to PyPI
49
+ uv publish
50
+
51
+ upgrade: ## Upgrade locked dependencies and pre-commit hook versions
52
+ uv lock --upgrade
53
+ $(RUN) pre-commit autoupdate
54
+
55
+ clean: ## Remove build artifacts and caches
56
+ rm -rf build dist .pytest_cache .ruff_cache .coverage htmlcov coverage.xml
57
+ find src tests -type d -name __pycache__ -exec rm -rf {} +
@@ -0,0 +1,80 @@
1
+ Metadata-Version: 2.5
2
+ Name: agentbuild
3
+ Version: 0.0.1
4
+ Summary: Tools to make agent research easier.
5
+ Author: trducng
6
+ Classifier: Operating System :: OS Independent
7
+ Classifier: Programming Language :: Python :: 3
8
+ Requires-Python: >=3.11
9
+ Requires-Dist: ipython>=8
10
+ Description-Content-Type: text/markdown
11
+
12
+ # agentbuild
13
+
14
+ Tools to make agent research easier.
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ pip install agentbuild
20
+ ```
21
+
22
+ ## Jupyter: `%load_cells`
23
+
24
+ Copy the code of cells from another notebook into the current cell, the way `%load` does for
25
+ files.
26
+
27
+ ```python
28
+ %load_ext agentbuild.jupyter
29
+
30
+ %load_cells other.ipynb # list the notebook's cells, IDs and titles
31
+ %load_cells other.ipynb --cells 7f3a9c21 b2e4d0aa # copy those cells into this cell
32
+ %load_cells "my export.ipynb" -c "Import Libraries" 7f3a
33
+ ```
34
+
35
+ Each cell after `--cells` (or `-c`) can be:
36
+
37
+ - a cell ID: the Jupyter cell ID, or for notebooks exported from Databricks, the Databricks cell ID
38
+ (`nuid`);
39
+ - the start of a cell ID, such as the 8 characters the listing shows;
40
+ - a Databricks cell title, matched exactly or ignoring case. Quote titles that contain spaces.
41
+
42
+ If a value matches more than one cell, the magic stops with an error instead of guessing.
43
+
44
+ Running the magic with `--cells` replaces the current cell with a commented-out copy of the magic
45
+ line, followed by the code of the chosen cells in the order given. Run the cell again to execute
46
+ that code. Relative paths are resolved from the kernel's working directory, which is normally the
47
+ notebook's folder.
48
+
49
+ To load the extension in every session, add this to
50
+ `~/.ipython/profile_default/ipython_config.py`:
51
+
52
+ ```python
53
+ c.InteractiveShellApp.extensions = ["agentbuild.jupyter"]
54
+ ```
55
+
56
+ ## Development
57
+
58
+ Requires [uv](https://docs.astral.sh/uv/) and `make`.
59
+
60
+ ```bash
61
+ make install # install the package + dev tools, set up git hooks
62
+ make help # list all commands
63
+ ```
64
+
65
+ Common commands:
66
+
67
+ ```bash
68
+ make test # run tests
69
+ make test-matrix # run tests on Python 3.11-3.15 (override with PYTHONS="3.11 3.12")
70
+ make cov # run tests with a coverage report
71
+ make lint # lint + format check
72
+ make format # auto-fix lint issues and format
73
+ make check # run the same checks as CI
74
+ ```
75
+
76
+ ## Release
77
+
78
+ ```bash
79
+ make publish # build and upload to PyPI (needs a PyPI token)
80
+ ```
@@ -0,0 +1,69 @@
1
+ # agentbuild
2
+
3
+ Tools to make agent research easier.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install agentbuild
9
+ ```
10
+
11
+ ## Jupyter: `%load_cells`
12
+
13
+ Copy the code of cells from another notebook into the current cell, the way `%load` does for
14
+ files.
15
+
16
+ ```python
17
+ %load_ext agentbuild.jupyter
18
+
19
+ %load_cells other.ipynb # list the notebook's cells, IDs and titles
20
+ %load_cells other.ipynb --cells 7f3a9c21 b2e4d0aa # copy those cells into this cell
21
+ %load_cells "my export.ipynb" -c "Import Libraries" 7f3a
22
+ ```
23
+
24
+ Each cell after `--cells` (or `-c`) can be:
25
+
26
+ - a cell ID: the Jupyter cell ID, or for notebooks exported from Databricks, the Databricks cell ID
27
+ (`nuid`);
28
+ - the start of a cell ID, such as the 8 characters the listing shows;
29
+ - a Databricks cell title, matched exactly or ignoring case. Quote titles that contain spaces.
30
+
31
+ If a value matches more than one cell, the magic stops with an error instead of guessing.
32
+
33
+ Running the magic with `--cells` replaces the current cell with a commented-out copy of the magic
34
+ line, followed by the code of the chosen cells in the order given. Run the cell again to execute
35
+ that code. Relative paths are resolved from the kernel's working directory, which is normally the
36
+ notebook's folder.
37
+
38
+ To load the extension in every session, add this to
39
+ `~/.ipython/profile_default/ipython_config.py`:
40
+
41
+ ```python
42
+ c.InteractiveShellApp.extensions = ["agentbuild.jupyter"]
43
+ ```
44
+
45
+ ## Development
46
+
47
+ Requires [uv](https://docs.astral.sh/uv/) and `make`.
48
+
49
+ ```bash
50
+ make install # install the package + dev tools, set up git hooks
51
+ make help # list all commands
52
+ ```
53
+
54
+ Common commands:
55
+
56
+ ```bash
57
+ make test # run tests
58
+ make test-matrix # run tests on Python 3.11-3.15 (override with PYTHONS="3.11 3.12")
59
+ make cov # run tests with a coverage report
60
+ make lint # lint + format check
61
+ make format # auto-fix lint issues and format
62
+ make check # run the same checks as CI
63
+ ```
64
+
65
+ ## Release
66
+
67
+ ```bash
68
+ make publish # build and upload to PyPI (needs a PyPI token)
69
+ ```
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "agentbuild"
7
+ version = "0.0.1"
8
+ description = "Tools to make agent research easier."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ authors = [{ name = "trducng" }]
12
+ classifiers = [
13
+ "Programming Language :: Python :: 3",
14
+ "Operating System :: OS Independent",
15
+ ]
16
+ dependencies = [
17
+ "ipython>=8",
18
+ ]
19
+
20
+ [dependency-groups]
21
+ dev = [
22
+ "pytest>=8",
23
+ "pytest-cov>=5",
24
+ "ruff>=0.8",
25
+ "pre-commit>=4",
26
+ ]
27
+
28
+ [tool.hatch.build.targets.wheel]
29
+ packages = ["src/agentbuild"]
30
+
31
+ [tool.pytest.ini_options]
32
+ testpaths = ["tests"]
33
+ addopts = "-ra"
34
+
35
+ [tool.ruff]
36
+ line-length = 100
37
+ target-version = "py311"
38
+
39
+ [tool.ruff.lint]
40
+ select = ["E", "F", "W", "I", "UP", "B", "SIM"]
@@ -0,0 +1,10 @@
1
+ """Tools to make agent research easier."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("agentbuild")
7
+ except PackageNotFoundError: # pragma: no cover - package not installed
8
+ __version__ = "0.0.0"
9
+
10
+ __all__ = ["__version__"]
@@ -0,0 +1,162 @@
1
+ """IPython magics for working with Jupyter notebooks.
2
+
3
+ Load them in a notebook with ``%load_ext agentbuild.jupyter``. Notebooks exported from
4
+ Databricks work too: their cells are identified by the Databricks cell ID (``nuid``) and can
5
+ also be referred to by their title.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from collections.abc import Callable
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from IPython.core.error import UsageError
16
+ from IPython.core.magic import Magics, line_magic, magics_class
17
+ from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring
18
+
19
+ Cell = dict[str, Any]
20
+
21
+ DATABRICKS_CELL_METADATA = "application/vnd.databricks.v1+cell"
22
+
23
+
24
+ def read_notebook_cells(path: str | Path) -> list[Cell]:
25
+ """Return the cells of a notebook file."""
26
+ with open(Path(path).expanduser(), encoding="utf-8") as f:
27
+ notebook = json.load(f)
28
+ if notebook.get("nbformat") != 4:
29
+ raise ValueError(f"{path} is not an nbformat 4 notebook")
30
+ return notebook["cells"]
31
+
32
+
33
+ def cell_source(cell: Cell) -> str:
34
+ source = cell.get("source", "")
35
+ return "".join(source) if isinstance(source, list) else source
36
+
37
+
38
+ def _databricks_metadata(cell: Cell) -> dict[str, Any]:
39
+ return cell.get("metadata", {}).get(DATABRICKS_CELL_METADATA) or {}
40
+
41
+
42
+ def cell_ids(cell: Cell) -> list[str]:
43
+ """Return the cell's Jupyter ID and Databricks ID (``nuid``), whichever it has."""
44
+ ids = [cell.get("id"), _databricks_metadata(cell).get("nuid")]
45
+ return [i for i in ids if i]
46
+
47
+
48
+ def cell_title(cell: Cell) -> str:
49
+ """Return the cell's Databricks title, or "" if it has none."""
50
+ return _databricks_metadata(cell).get("title") or ""
51
+
52
+
53
+ def find_cell(cells: list[Cell], ref: str) -> Cell:
54
+ """Return the one cell that ``ref`` names.
55
+
56
+ ``ref`` is tried, in order, as an exact cell ID, an exact Databricks title, a title
57
+ ignoring case, and the start of a cell ID.
58
+ """
59
+ matchers: list[Callable[[Cell], bool]] = [
60
+ lambda cell: ref in cell_ids(cell),
61
+ lambda cell: ref == cell_title(cell),
62
+ lambda cell: ref.casefold() == cell_title(cell).casefold(),
63
+ lambda cell: any(i.startswith(ref) for i in cell_ids(cell)),
64
+ ]
65
+ for matches in matchers:
66
+ positions = [i for i, cell in enumerate(cells) if matches(cell)]
67
+ if len(positions) == 1:
68
+ return cells[positions[0]]
69
+ if positions:
70
+ listed = ", ".join(f"#{i}" for i in positions)
71
+ raise ValueError(f"{ref!r} matches more than one cell: {listed}")
72
+ hint = ""
73
+ if not any(map(cell_ids, cells)):
74
+ hint = " (the notebook has no cell IDs; open and save it in a recent Jupyter to add them)"
75
+ raise ValueError(f"no cell matches {ref!r}{hint}")
76
+
77
+
78
+ def join_cells(cells: list[Cell], refs: list[str]) -> str:
79
+ """Return the code of the cells that ``refs`` name, joined in the order given."""
80
+ chosen = [find_cell(cells, ref) for ref in refs]
81
+ for ref, cell in zip(refs, chosen, strict=True):
82
+ cell_type = cell.get("cell_type")
83
+ if cell_type != "code":
84
+ raise ValueError(f"{ref!r} is a {cell_type} cell, not a code cell")
85
+ return "\n\n".join(cell_source(cell).strip("\n") for cell in chosen)
86
+
87
+
88
+ def short_ids(ids: list[str | None]) -> list[str]:
89
+ """Shorten IDs to the shortest length, at least 8, at which they stay unique."""
90
+ real = [i for i in ids if i]
91
+ length = 8
92
+ while len({i[:length] for i in real}) < len(set(real)):
93
+ length += 1
94
+ return [i[:length] if i else "-" for i in ids]
95
+
96
+
97
+ def format_cell_table(cells: list[Cell]) -> str:
98
+ """Return a table of each cell's position, ID, type, title and first line."""
99
+ ids = short_ids([next(iter(cell_ids(cell)), None) for cell in cells])
100
+ titles = [cell_title(cell) for cell in cells]
101
+ has_titles = any(titles)
102
+ header = ["#", "id", "type", *(["title"] if has_titles else []), "first line"]
103
+ rows = [header]
104
+ for i, (cell, short_id, title) in enumerate(zip(cells, ids, titles, strict=True)):
105
+ first_line = next(iter(cell_source(cell).strip().splitlines()), "")
106
+ title_column = [title[:40]] if has_titles else []
107
+ rows.append([str(i), short_id, cell.get("cell_type", "?"), *title_column, first_line[:60]])
108
+ widths = [max(len(row[k]) for row in rows) for k in range(len(header))]
109
+ return "\n".join(
110
+ " ".join(value.ljust(width) for value, width in zip(row, widths, strict=True)).rstrip()
111
+ for row in rows
112
+ )
113
+
114
+
115
+ def _unquote(value: str) -> str:
116
+ # IPython splits magic arguments without removing quotes, so that Windows paths keep
117
+ # their backslashes. Remove one pair of matching quotes here instead.
118
+ if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'":
119
+ return value[1:-1]
120
+ return value
121
+
122
+
123
+ @magics_class
124
+ class NotebookMagics(Magics):
125
+ @magic_arguments()
126
+ @argument("path", help="Notebook file, absolute or relative to the working directory.")
127
+ @argument(
128
+ "-c",
129
+ "--cells",
130
+ nargs="+",
131
+ metavar="CELL",
132
+ help=(
133
+ "Cells to copy, in the order to paste them. Each is a cell ID, the start of one, "
134
+ "or a Databricks cell title (quote titles that contain spaces)."
135
+ ),
136
+ )
137
+ @line_magic
138
+ def load_cells(self, line: str) -> None:
139
+ """Copy the code of cells from another notebook into the current cell.
140
+
141
+ Like %load, this replaces the current cell with a commented-out copy of
142
+ the magic line followed by the copied code. Without --cells, it lists
143
+ the notebook's cells, with their IDs and titles, instead.
144
+ """
145
+ args = parse_argstring(self.load_cells, line)
146
+ try:
147
+ cells = read_notebook_cells(_unquote(args.path))
148
+ except (OSError, ValueError) as e:
149
+ raise UsageError(str(e)) from e
150
+ if not args.cells:
151
+ print(format_cell_table(cells))
152
+ return
153
+ try:
154
+ code = join_cells(cells, [_unquote(ref) for ref in args.cells])
155
+ except ValueError as e:
156
+ hint = f"Run `%load_cells {args.path}` to list the notebook's cells."
157
+ raise UsageError(f"{e}\n{hint}") from e
158
+ self.shell.set_next_input(f"# %load_cells {line}\n{code}", replace=True)
159
+
160
+
161
+ def load_ipython_extension(ipython: Any) -> None:
162
+ ipython.register_magics(NotebookMagics)
File without changes
@@ -0,0 +1,6 @@
1
+ import agentbuild
2
+
3
+
4
+ def test_version() -> None:
5
+ assert isinstance(agentbuild.__version__, str)
6
+ assert agentbuild.__version__