├── LICENSE
├── README.md
├── eip
├── dag
│ ├── __init__.py
│ ├── cache.py
│ └── node.py
├── eip.css
├── index.html
├── ltk
│ ├── __init__.py
│ ├── element.py
│ ├── jquery.py
│ ├── ltk.css
│ └── widgets.py
├── main.py
└── pyscript.toml
└── example-1.png
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 laffra
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Excel in Python
2 |
3 | This project is obsolete and replaced with [PySheets](https://github.com/pysheets/pysheets).
4 |
5 | - This project is not associated with Microsoft Corporation.
6 | - Excel is a registered trademark of Microsoft Corporation.
7 |
--------------------------------------------------------------------------------
/eip/dag/__init__.py:
--------------------------------------------------------------------------------
1 | from .node import Node, NotSet
2 | from .cache import memoize, clear
--------------------------------------------------------------------------------
/eip/dag/cache.py:
--------------------------------------------------------------------------------
1 | cache = {}
2 |
3 | def memoize(function):
4 | def inner(*args):
5 | try:
6 | value = cache[args[0]]
7 | except KeyError:
8 | value = cache[args[0]] = function(*args)
9 | return value
10 | return inner
11 |
12 |
13 | def clear(key):
14 | try:
15 | del cache[key]
16 | except:
17 | pass
--------------------------------------------------------------------------------
/eip/dag/node.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import inspect
4 | from dag import cache
5 |
6 |
7 | class Node():
8 | inputs = []
9 | dependents = set()
10 | listeners = []
11 | error = False
12 |
13 | def __init__(self: Node, *args):
14 | self.dependents = set()
15 | self.set_inputs(set(arg for arg in args if isinstance(arg, Node)))
16 | self.set_listeners(set(arg for arg in args if inspect.ismethod(arg)))
17 | for value in set(args) - self.inputs - self.listeners:
18 | self.set(value)
19 |
20 | def set_inputs(self, inputs):
21 | self.inputs = inputs
22 | for input in inputs:
23 | input.dependents.add(self)
24 |
25 | def set_listeners(self, listeners):
26 | self.listeners = listeners
27 |
28 | @cache.memoize
29 | def get(self):
30 | pass
31 |
32 | def reset(self):
33 | self.clear()
34 | self.error = False
35 | self.notify_listeners()
36 |
37 | def clear(self):
38 | if self in cache.cache:
39 | del cache.cache[self]
40 | self.notify_dependents()
41 |
42 | def notify_dependents(self):
43 | for dependent in self.dependents:
44 | dependent.reset()
45 |
46 | def set(self, value):
47 | self.clear()
48 | cache.cache[self] = value
49 | self.notify_listeners()
50 | self.notify_dependents()
51 |
52 | def notify_listeners(self):
53 | for listener in self.listeners:
54 | listener()
55 |
56 | def __hash__(self):
57 | return id(self)
58 |
59 | def __repr__(self):
60 | return f"<{self.__class__.__name__}={cache.cache.get(self, '?')}>"
61 |
62 |
63 | class NotSet(Node):
64 | pass
--------------------------------------------------------------------------------
/eip/eip.css:
--------------------------------------------------------------------------------
1 | .spreadsheet-blank, .spreadsheet-row-label {
2 | padding: 6px;
3 | width: 30px;
4 | height: 26px;
5 | margin: 0 0 3px 0;
6 | color: black;
7 | border: 0 solid transparent;
8 | border-width: 0 0 2px 0;
9 | background-color: transparent;
10 | }
11 |
12 | .spreadsheet-row-label:hover {
13 | border-color: #AAF;
14 | background-color: #EEE;
15 | }
16 |
17 | .spreadsheet-column-label {
18 | padding: 4px 4px 4px 6px;
19 | margin: 4px 0;
20 | width: 30px;
21 | height: 30px;
22 | color: black;
23 | text-align: center;
24 | border: 0 solid transparent;
25 | border-width: 0 2px 0 0;
26 | background-color: transparent;
27 | }
28 |
29 | .spreadsheet-column-label:hover {
30 | border-color: #AAF;
31 | background-color: #EEE;
32 | }
33 |
34 | h3 {
35 | color: grey;
36 | font-weight: 100;
37 | }
38 |
39 | #editor, #value {
40 | width: 436px;
41 | border: 1px solid #DDD;
42 | font-family: 'Courier New', Courier, monospace;
43 | }
44 |
45 | #value {
46 | color: #AAA;
47 | height: 24px;
48 | }
--------------------------------------------------------------------------------
/eip/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Excel in Python 🤯
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | Excel in Python 🤯
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/eip/ltk/__init__.py:
--------------------------------------------------------------------------------
1 | from .element import Element
2 | from .jquery import find, later, on
3 | from .widgets import VBox, HBox, Text, Input
--------------------------------------------------------------------------------
/eip/ltk/element.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 | import js # type: ignore
3 | import pyodide # type: ignore
4 | from typing import Iterable
5 |
6 |
7 | class Element(object):
8 | classes = []
9 | element = None
10 | tag = "div"
11 |
12 | def __init__(self, *children):
13 | self.element = js.jQuery(f"""
14 | <{self.tag} class='{" ".join(self.classes)}'>
15 | """).append(*self.expand(children))
16 |
17 | def expand(self, children_list):
18 | result = []
19 | for item in children_list:
20 | try:
21 | if isinstance(item, (Element, pyodide.ffi.JsProxy)):
22 | result.append(item)
23 | elif isinstance(item, Iterable):
24 | result.extend(item)
25 | except Exception as e:
26 | print(f"{e}: Argument should be an Element or a list, not {type(item)}, {item}")
27 | return result
28 |
29 | def render(self, element_id):
30 | self.element.appendTo(js.jQuery(element_id))
31 |
32 | def on(self, kind, handler):
33 | self.element.on(kind, pyodide.ffi.create_proxy(lambda event: handler()))
34 | return self
35 |
36 | def __getattribute__(self, name: str) -> Any:
37 | try:
38 | return object.__getattribute__(self, name)
39 | except:
40 | return getattr(self.element, name)
41 |
--------------------------------------------------------------------------------
/eip/ltk/jquery.py:
--------------------------------------------------------------------------------
1 | import js # type: ignore
2 | import pyodide # type: ignore
3 |
4 |
5 | def find(selector):
6 | return js.jQuery(selector)
7 |
8 | def later(function, timeout=1):
9 | js.setTimeout(pyodide.ffi.create_proxy(function), timeout)
10 |
11 | def on(element, kind, handler):
12 | element.on(kind, pyodide.ffi.create_proxy(lambda event: handler()))
--------------------------------------------------------------------------------
/eip/ltk/ltk.css:
--------------------------------------------------------------------------------
1 | .vbox {
2 | display: flex;
3 | flex-direction: column;
4 | align-content: flex-start;
5 | gap: 0;
6 | }
7 |
8 | .hbox {
9 | display: flex;
10 | flex-direction: row;
11 | align-content: flex-start;
12 | margin-top: -4px;
13 | }
14 |
15 | .text {
16 | border: 1px solid #DDD;
17 | text-align: left;
18 | width: 36px;
19 | height: 32px;
20 | padding: 2px;
21 | }
22 |
23 | .text:hover {
24 | background-color: bisque;
25 | }
26 |
27 | .input {
28 | border: 1px solid #DDD;
29 | width: 36px;
30 | height: 32px;
31 | }
32 |
33 | py-terminal {
34 | display: none;
35 | }
--------------------------------------------------------------------------------
/eip/ltk/widgets.py:
--------------------------------------------------------------------------------
1 | from ltk.element import Element
2 |
3 |
4 | class HBox(Element):
5 | classes = [ "hbox" ]
6 |
7 |
8 | class VBox(Element):
9 | classes = [ "vbox" ]
10 |
11 |
12 | class Text(Element):
13 | classes = [ "text" ]
14 |
15 | def __init__(self, html=""):
16 | Element.__init__(self)
17 | self.element.html(html)
18 |
19 |
20 | class Input(Element):
21 | classes = [ "input" ]
22 | tag = "input"
23 |
24 | def __init__(self, value=""):
25 | Element.__init__(self)
26 | self.element.val(value)
--------------------------------------------------------------------------------
/eip/main.py:
--------------------------------------------------------------------------------
1 | from ltk import later, find, on, HBox, Input, Text, VBox
2 | from dag import Node, NotSet, memoize, clear
3 | import re
4 |
5 | ROW_COUNT = 5
6 | COLUMN_COUNT = 5
7 |
8 | def convert(cell):
9 | try:
10 | return float(cell)
11 | except:
12 | return cell
13 |
14 | class SpreadsheetCellModel(Node):
15 | models = {}
16 | script = None
17 |
18 | def __init__(self, key, value, listener):
19 | self.key = key
20 | self.models[key] = self
21 | Node.__init__(self, value, listener)
22 |
23 | def set(self, value):
24 | self.formula = None
25 | if value and value[0] == "=":
26 | value = value[1:]
27 | self.formula = value
28 | self.script = re.sub("([A-Z]+[0-9]+)", r"convert(SpreadsheetCellModel.models['\1'].get())", self.formula)
29 | inputs = [SpreadsheetCellModel.models[key] for key in re.findall("([A-Z]+[0-9]+)", value)]
30 | self.set_inputs(inputs)
31 | Node.set(self, value)
32 | for listener in self.listeners:
33 | listener()
34 |
35 | def get(self):
36 | try:
37 | self.error = False
38 | return eval(self.script) if self.script else ""
39 | except Exception as e:
40 | self.error = True
41 | return f"😞 {e}"
42 | def __repr__(self):
43 | return f""
44 |
45 |
46 | class Blank(Text):
47 | classes = [ "spreadsheet-blank" ]
48 |
49 |
50 | def resizable(element, handles, rowOrColumn, index):
51 | print('resizable', element, index)
52 | element.resizable()
53 | element.resizable("option", "handles", handles)
54 | element.resizable("option", "alsoResize", f".{rowOrColumn}-{index}")
55 |
56 |
57 | class RowLabel(Text):
58 | classes = [ "spreadsheet-row-label" ]
59 |
60 | def __init__(self, value, index):
61 | Text.__init__(self, value)
62 | resizable(self.element, "s", "row", index)
63 |
64 |
65 | class ColumnLabel(Text):
66 | classes = [ "spreadsheet-column-label" ]
67 |
68 | def __init__(self, value, index):
69 | Text.__init__(self, value)
70 | resizable(self.element, "e", "col", index)
71 |
72 |
73 | class SpreadsheetCell(Input):
74 | cells = {}
75 | models = {}
76 | model = NotSet()
77 | updating = False
78 | current = None
79 |
80 | def __init__(self, value, row, column):
81 | Input.__init__(self)
82 | self.column = column
83 | self.row = row
84 | key = f"{chr(ord('A') + column)}{row}"
85 | SpreadsheetCell.cells[key] = self
86 | SpreadsheetCell.models[key] = self.model = SpreadsheetCellModel(key, value, self.model_changed)
87 | (self
88 | .on("focus", self.set_editor)
89 | .on("change", self.view_changed)
90 | .addClass(f"col-{column}")
91 | .addClass(f"row-{row}")
92 | .width(36)
93 | .height(34)
94 | .attr("id", key))
95 | self.model_changed()
96 |
97 | def set_editor(self):
98 | SpreadsheetCell.current = self
99 | find("#editor").val(self.model.formula or self.model.get())
100 | find("#value").text(self.model.get()).css("color", "#F99" if self.model.error else "#AAA")
101 |
102 | def model_changed(self):
103 | if self.updating: later(self.model_changed)
104 | try:
105 | self.updating = True
106 | self.element.val("😞" if self.model.error else self.model.get())
107 | self.element.css("color", "#F99" if self.model.error else "black")
108 | finally:
109 | self.updating = False
110 |
111 | def view_changed(self):
112 | if self.updating: return
113 | try:
114 | self.updating = True
115 | self.model.set(self.element.val())
116 | self.set_editor()
117 |
118 | finally:
119 | self.updating = False
120 |
121 |
122 | def create_row(row_index):
123 | row_label = RowLabel(row_index, row_index)
124 | cells = [ SpreadsheetCell("", row_index, column) for column in range(COLUMN_COUNT) ]
125 | return HBox(row_label, cells)
126 |
127 | def update_cell():
128 | SpreadsheetCell.current.model.set(find("#editor").val())
129 |
130 | def create_spreadsheet():
131 | editor = Input().attr("id", "editor")
132 | value = Text().css("border", "").attr("id", "value")
133 | blank = Blank()
134 | headers = HBox(blank, [ ColumnLabel(chr(ord('A') + column), column) for column in range(COLUMN_COUNT) ])
135 | rows = [ create_row(row) for row in range(1, COLUMN_COUNT) ]
136 | VBox(editor, value, headers, rows).render("#main")
137 | SpreadsheetCell.models["A1"].set("5")
138 | SpreadsheetCell.models["B1"].set("4")
139 | SpreadsheetCell.models["C1"].set("=A1+B1")
140 | SpreadsheetCell.models["A2"].set("'hello'")
141 | SpreadsheetCell.models["B2"].set("'world'")
142 | SpreadsheetCell.models["C2"].set("=A2.capitalize() + ' ' + B2.upper()")
143 | on(editor, "change", update_cell)
144 |
145 | create_spreadsheet()
--------------------------------------------------------------------------------
/eip/pyscript.toml:
--------------------------------------------------------------------------------
1 | [[fetch]]
2 | files = [
3 | "./dag/__init__.py",
4 | "./dag/cache.py",
5 | "./dag/node.py",
6 |
7 | "./ltk/__init__.py",
8 | "./ltk/jquery.py",
9 | "./ltk/element.py",
10 | "./ltk/widgets.py",
11 | ]
--------------------------------------------------------------------------------
/example-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/laffra/excel-in-python/7e5e35b55adaa7c04eeb5733f5ccb12868c8ef8d/example-1.png
--------------------------------------------------------------------------------