├── 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 --------------------------------------------------------------------------------