modeleon 0.1.0__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,18 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ .mypy_cache/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+
11
+ # AI coding tools — excluded from open-source extraction
12
+ CLAUDE.md
13
+ .claude/
14
+ .cursor/
15
+ .cursorignore
16
+ .cursorules
17
+ .copilot/
18
+ .github/copilot/
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.4
2
+ Name: modeleon
3
+ Version: 0.1.0
4
+ Summary: Financial Model Engineering — Python code compiles to live Excel formulas
5
+ License: Apache-2.0
6
+ Requires-Python: >=3.12
7
+ Requires-Dist: openpyxl>=3.1
8
+ Requires-Dist: pydantic>=2.0
9
+ Provides-Extra: dev
10
+ Requires-Dist: hypothesis>=6.0; extra == 'dev'
11
+ Requires-Dist: mypy>=1.10; extra == 'dev'
12
+ Requires-Dist: pytest>=8.0; extra == 'dev'
13
+ Requires-Dist: ruff>=0.4; extra == 'dev'
@@ -0,0 +1,63 @@
1
+ # Modeleon
2
+
3
+ **Financial Model Engineering — Python code compiles to live Excel formulas.**
4
+
5
+ Write financial models in Python. Get real, auditable Excel formulas — not dead values.
6
+
7
+ ```python
8
+ from modeleon import Model
9
+
10
+ with Model("Revenue Forecast") as m:
11
+ with m.section("Assumptions"):
12
+ price = m.var("Price per Unit", 50, style="input")
13
+ volume = m.var("Volume", 1000, style="input")
14
+ growth = m.var("Growth Rate", 0.10, style="input")
15
+
16
+ with m.section("Revenue"):
17
+ revenue = m.var("Revenue", price * volume, style="total")
18
+ next_year = m.var("Next Year", revenue * (1 + growth))
19
+
20
+ m.to_excel("revenue.xlsx")
21
+ ```
22
+
23
+ `revenue.xlsx` contains `=B2*B3`, not `50000`. Every cell is a live formula you can audit, extend, and trust.
24
+
25
+ ## Why
26
+
27
+ Financial models are the DNA of business decisions. They live in spreadsheets — unversioned, error-prone, disconnected from data. [94% of spreadsheets contain errors](https://link.springer.com/article/10.1007/s11704-023-2384-6).
28
+
29
+ Modeleon brings software engineering to financial modeling without leaving Excel. See the [manifesto](https://github.com/modeleonai/modeleon/blob/main/manifesto.md).
30
+
31
+ ## Features
32
+
33
+ - **Python-Excel parity** — every Python expression produces a real Excel formula
34
+ - **Symbolic compilation** — two-pass compiler: layout first, then formulas
35
+ - **Time as a first-class dimension** — monthly/quarterly/annual, actuals/forecast boundary, auto-aggregation
36
+ - **Style presets** — `input`, `total`, `grand_total`, `computed` — automatic Excel formatting
37
+ - **Named and unnamed variables** — free-form cells are real graph nodes with full dependency tracking
38
+ - **Graph-native** — the model is a DAG, enabling traversal, impact analysis, and AI integration
39
+ - **Plugin system** — extensible via entry points
40
+
41
+ ## Install
42
+
43
+ ```bash
44
+ pip install modeleon
45
+ ```
46
+
47
+ ## CLI
48
+
49
+ ```bash
50
+ modeleon build model.py -o output.xlsx
51
+ ```
52
+
53
+ ## How it works
54
+
55
+ 1. You write a model in Python using the DSL
56
+ 2. The DSL builds a hierarchical expression graph (Model -> Section -> Variable)
57
+ 3. Pass 1 (layout): assigns Excel coordinates `(sheet, row, col)` from hierarchy
58
+ 4. Pass 2 (formulas): emits Excel formulas using those addresses
59
+ 5. The Excel writer produces a fully functional `.xlsx` file
60
+
61
+ ## License
62
+
63
+ MIT
@@ -0,0 +1,39 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "modeleon"
7
+ version = "0.1.0"
8
+ description = "Financial Model Engineering — Python code compiles to live Excel formulas"
9
+ requires-python = ">=3.12"
10
+ license = {text = "Apache-2.0"}
11
+ dependencies = [
12
+ "pydantic>=2.0",
13
+ "openpyxl>=3.1",
14
+ ]
15
+
16
+ [project.optional-dependencies]
17
+ dev = [
18
+ "pytest>=8.0",
19
+ "hypothesis>=6.0",
20
+ "ruff>=0.4",
21
+ "mypy>=1.10",
22
+ ]
23
+
24
+ [project.entry-points."modeleon.plugins"]
25
+ # Pro registers here: pro = "modeleon_pro.plugin"
26
+
27
+ [tool.hatch.build.targets.wheel]
28
+ packages = ["src/modeleon"]
29
+
30
+ [tool.pytest.ini_options]
31
+ testpaths = ["tests"]
32
+
33
+ [tool.ruff]
34
+ line-length = 100
35
+ target-version = "py311"
36
+
37
+ [tool.mypy]
38
+ python_version = "3.12"
39
+ strict = true
@@ -0,0 +1,7 @@
1
+ """Modeleon — Financial Model Engineering. Python code compiles to live Excel formulas."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ from modeleon.plugins import load_plugins as _load_plugins
6
+
7
+ _load_plugins()
@@ -0,0 +1,65 @@
1
+ """Plugin extension registry for modeleon.
2
+
3
+ Pro and other extensions register capabilities here via Python entry points.
4
+ Users always write `from modeleon import Model` — pro features appear automatically
5
+ when modeleon-pro is installed.
6
+
7
+ See ADR-006 for the design rationale.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import importlib.metadata
13
+ import logging
14
+ from typing import Any
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ _registry: dict[str, Any] = {
19
+ # Extra kwargs accepted by Variable (e.g., control=, access=, style=)
20
+ "variable_kwargs": {},
21
+ # Additional compiler passes (run after core compilation)
22
+ "compiler_passes": [],
23
+ # Additional node validators (run on graph validation)
24
+ "node_validators": [],
25
+ # Track which plugins have loaded
26
+ "_loaded": [],
27
+ }
28
+
29
+ _plugins_loaded = False
30
+
31
+
32
+ def get_registry() -> dict[str, Any]:
33
+ """Return the plugin registry. Read-only access for inspection and tests."""
34
+ return _registry
35
+
36
+
37
+ def load_plugins() -> None:
38
+ """Discover and load all installed plugins from the 'modeleon.plugins' entry point group.
39
+
40
+ Called once on engine import. Safe to call multiple times (idempotent).
41
+ """
42
+ global _plugins_loaded
43
+ if _plugins_loaded:
44
+ return
45
+ _plugins_loaded = True
46
+
47
+ eps = importlib.metadata.entry_points(group="modeleon.plugins")
48
+ for ep in eps:
49
+ try:
50
+ plugin_module = ep.load()
51
+ plugin_module.register(_registry)
52
+ _registry["_loaded"].append(ep.name)
53
+ logger.debug("Loaded plugin: %s", ep.name)
54
+ except Exception:
55
+ logger.warning("Failed to load plugin: %s", ep.name, exc_info=True)
56
+
57
+
58
+ def reset_registry() -> None:
59
+ """Reset the registry to its initial state. For testing only."""
60
+ global _plugins_loaded
61
+ _registry["variable_kwargs"].clear()
62
+ _registry["compiler_passes"].clear()
63
+ _registry["node_validators"].clear()
64
+ _registry["_loaded"].clear()
65
+ _plugins_loaded = False
@@ -0,0 +1,32 @@
1
+ from modeleon.plugins import get_registry, load_plugins, reset_registry
2
+
3
+
4
+ def test_registry_has_expected_categories():
5
+ registry = get_registry()
6
+ assert "variable_kwargs" in registry
7
+ assert "compiler_passes" in registry
8
+ assert "node_validators" in registry
9
+ assert "_loaded" in registry
10
+
11
+
12
+ def test_load_plugins_is_idempotent():
13
+ reset_registry()
14
+ load_plugins()
15
+ loaded_first = list(get_registry()["_loaded"])
16
+ load_plugins()
17
+ loaded_second = list(get_registry()["_loaded"])
18
+ assert loaded_first == loaded_second
19
+
20
+
21
+ def test_mock_plugin_registers():
22
+ reset_registry()
23
+ registry = get_registry()
24
+
25
+ def mock_register(reg: dict) -> None:
26
+ reg["variable_kwargs"]["test_kwarg"] = "test_handler"
27
+
28
+ mock_register(registry)
29
+ assert registry["variable_kwargs"]["test_kwarg"] == "test_handler"
30
+
31
+ # Clean up
32
+ reset_registry()