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.
- modeleon-0.1.0/.gitignore +18 -0
- modeleon-0.1.0/PKG-INFO +13 -0
- modeleon-0.1.0/README.md +63 -0
- modeleon-0.1.0/pyproject.toml +39 -0
- modeleon-0.1.0/src/modeleon/__init__.py +7 -0
- modeleon-0.1.0/src/modeleon/plugins.py +65 -0
- modeleon-0.1.0/tests/test_plugins.py +32 -0
|
@@ -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/
|
modeleon-0.1.0/PKG-INFO
ADDED
|
@@ -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'
|
modeleon-0.1.0/README.md
ADDED
|
@@ -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,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()
|