llusen 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.
llusen-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Anthony Blackshaw
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.
llusen-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,198 @@
1
+ Metadata-Version: 2.4
2
+ Name: llusen
3
+ Version: 0.1.0
4
+ Summary: Read and change the project open in llusen from Python
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.14
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Provides-Extra: pandas
10
+ Requires-Dist: pandas>=2.2; extra == "pandas"
11
+ Dynamic: license-file
12
+
13
+ # llusen for Python
14
+
15
+ Read and change the project open in llusen from Python: tables, formulas,
16
+ filters, summary values, group tables, charts and notes; and export them
17
+ as CSV, Excel or OpenDocument files.
18
+
19
+ ```python
20
+ import llusen
21
+
22
+ with llusen.connect() as project:
23
+ orders = project.table("orders")
24
+ orders.add_column("total", "price * qty")
25
+ orders.set_format("total", 'currency("£", 2)')
26
+ print(orders.summary())
27
+ frame = orders.to_pandas() # with pandas installed
28
+ ```
29
+
30
+ ## Setting up
31
+
32
+ - **Python 3.14** or later. The package uses only the standard library;
33
+ pandas is optional, for `to_pandas` and for giving DataFrames.
34
+ - **Install** from PyPI: `pip install llusen`, or `pip install
35
+ "llusen[pandas]"` with pandas. From this folder: `pip install .`.
36
+ - **In llusen**, choose **Tools > API Enabled**. llusen then listens on
37
+ this machine only, and writes its port and a token to `api.json` in its
38
+ state folder:
39
+ - Linux: `~/.local/state/llusen` (or `$XDG_STATE_HOME/llusen`);
40
+ - macOS: `~/Library/Application Support/llusen`;
41
+ - Windows: `%LOCALAPPDATA%\llusen`.
42
+
43
+ `llusen.connect()` reads that file and gives the token. API Enabled is
44
+ remembered, and the token is new each time it is turned on.
45
+
46
+ - **Which llusen:** each project opens in a llusen of its own, with its
47
+ own API, turned on in its own window. `connect()` reaches the llusen
48
+ used last (the one whose window last had the keyboard), as `api.json`
49
+ names it. To reach the llusen that has a given project open, name the
50
+ project's file:
51
+
52
+ ```python
53
+ with llusen.connect(project="~/reports/sales.llu") as project:
54
+ ...
55
+ ```
56
+
57
+ If no llusen has it open, or the one that has has its API off,
58
+ `NotRunning` says which. `llusen.find_project(path)` gives that
59
+ llusen's port and token without connecting. (Each llusen lists what
60
+ it has open in `instances` in the state folder.)
61
+
62
+ ## How it works
63
+
64
+ - Changes go into the project as if made in llusen. Each is one step
65
+ that Ctrl+Z takes back in that table, and the status bar says what the
66
+ script did. A table a script makes opens in a tab.
67
+ - Rows are counted in the order they were loaded or added, from 0,
68
+ whatever the user's sort and filters show. Reading gives every row.
69
+ - Reading waits until formulas are worked out, so values are never half
70
+ done. llusen gives up on a call after five minutes of waiting.
71
+ - A call llusen refuses raises `LlusenError`, with llusen's own message.
72
+ For a formula, `error.at` is the (start, end) of the problem in it.
73
+ - If llusen isn't running, or API Enabled is off, `connect` raises
74
+ `NotRunning` (a `ConnectionError`).
75
+
76
+ ## Exporting
77
+
78
+ llusen writes the file, so the path is on the machine llusen runs on. A
79
+ relative path, or one from `~`, is made whole from the script's folder
80
+ first. A file already there is replaced, through a temporary file, so a
81
+ failed write leaves it as it was. Rows go in the order llusen shows them
82
+ (sorted, every row, whatever the filters). llusen writes on a thread of
83
+ its own and answers once the file is complete; the status bar says so.
84
+ The path's extension decides the kind (`.csv`, `.xlsx`, `.ods`), unless
85
+ `format` says; a path with no extension gets the format's.
86
+
87
+ ## Values
88
+
89
+ | llusen | Python | pandas |
90
+ |----------|-------------------------------|---------------------------|
91
+ | int | `int` | `int64` (`Int64` if any are missing) |
92
+ | float | `float` | `float64` |
93
+ | bool | `bool` | `bool` (`boolean` if any are missing) |
94
+ | text | `str` | `object` |
95
+ | date | `datetime.date` | `datetime64` |
96
+ | datetime | `datetime.datetime` (UTC, aware, if the column is UTC) | `datetime64` |
97
+ | time | `datetime.time` | `object` (`datetime.time`) |
98
+ | duration | `datetime.timedelta` | `timedelta64` |
99
+ | null | `None` | missing |
100
+ | a fault such as `#DIV0` | `llusen.Fault("#DIV0")` | missing |
101
+
102
+ When giving values, NaN, `NaT` and `pd.NA` are null; numpy's numbers are
103
+ numbers; a naive datetime is sent as it is, and an aware one in UTC;
104
+ pandas' `Timedelta` and numpy's `timedelta64` are durations (to the
105
+ microsecond), and a time of day must have no time zone. A
106
+ new column's type is worked out from its values (whole numbers make
107
+ `int`, any decimal makes `float`), unless named in `types`.
108
+
109
+ ## Reference
110
+
111
+ `llusen.connect(port=None, token=None, info_file=None, timeout=360,
112
+ project=None)` gives a `Project`: with `project`, the llusen that has
113
+ that project file open. Use it in a `with` block, or call `close()`. One
114
+ connection is for one thread at a time.
115
+
116
+ **Project**
117
+ - `describe()`: every table as llusen describes it (columns, types,
118
+ formulas, formats, filters, sort, summary values, groupings, charts,
119
+ notes).
120
+ - `table_names()`, `table(name)`, `chart(name)`.
121
+ - `new_table(name, data, types=None)`: `data` is a DataFrame, or a dict
122
+ of column names to lists of values.
123
+ - `new_group_table(name, grouping)`, e.g.
124
+ `'table("orders").group(r => ({ Item: r.item }))'`.
125
+ - `new_chart(name, table, x, ys, kind=None)`: `ys` is a column or a list
126
+ of them; `kind` is `"bar"`, `"line"` or `"doughnut"` (a doughnut
127
+ takes one column of values).
128
+ - `export(path, format=None, tables=None)`: every table (or those
129
+ named) to an Excel (`.xlsx`) or OpenDocument (`.ods`) workbook, a sheet
130
+ each, with summary values at the top and column formats; charts are
131
+ left out. Gives `{"path", "rows", "sheets"}` once written.
132
+ - `call(method, **params)`: any API method, as llusen documents it.
133
+
134
+ **Table**
135
+ - `read(columns=None, start=0, count=None)`: `{name: [values]}`, read in
136
+ pieces of 100,000 rows. `to_pandas(columns=None)`.
137
+ - `columns`, `len(table)`, `summary()` (values by name), `describe()`.
138
+ - `add_column(name, formula=None, values=None, type=None)`,
139
+ `set_formula`, `remove_formula`, `rename_column`,
140
+ `delete_columns(*names)`, `set_format(column, format=None)`.
141
+ - `append_rows(data)` gives the first new row; `set_cells(column,
142
+ values, start=0)`, all or none.
143
+ - `change_case(column, case)` puts every cell of a text column in
144
+ `"upper"`, `"lower"`, `"capitalize"` (`Hello world`) or `"title"`
145
+ (`Hello World`) case, and gives how many changed. The formula
146
+ functions of the same names do it in a new column instead.
147
+ - Live rows: `with table.locked(): table.insert_rows(data)`, or
148
+ `lock()`, `insert_rows(data)` and `unlock()`. While a script has a
149
+ table locked, rows go in at its end, many times a second if need be,
150
+ with no undo steps; llusen lifts any sort and filters, and no one else
151
+ can change the table (the user watches it fill, scrolled to the end).
152
+ `insert_rows` is answered at once, with the row its first will be at,
153
+ and its rows go in as soon as the table, and the tables reading it,
154
+ have worked out the last ones. `unlock` waits until all are in. The
155
+ lock goes if the script's connection closes. `is_locked` says whether
156
+ a script has it.
157
+ - `lock(clear=False, columns=None)`: `columns` are the rows' columns and
158
+ types, as `{"price": "float", "side": "text"}`. With `clear=True`, the
159
+ table is emptied for a new run and its plain columns made exactly
160
+ those, in that order (formula columns stay, after them), with no undo
161
+ step; it gives what changed. Without, they are checked against the
162
+ table's, and a difference refuses the lock. `locked()` takes the same.
163
+ - `set_filter(column, formula=None)` gives the rows shown; `value` is the
164
+ column's cell, as in `"value > 5"`. `clear_filters()`.
165
+ - `add_summary(formula, name=None, format=None)` gives its name;
166
+ `set_summary_formula`, `set_summary_format`, `delete_summary`.
167
+ - `set_grouping(grouping)` for a group table.
168
+ - `export(path, format=None)`: as CSV, or as a workbook of this table if
169
+ the path ends in `.xlsx` or `.ods` (or `format` says so).
170
+ - `rename(name)`, `delete()`.
171
+
172
+ **Chart**
173
+ - `notes()`, `add_note(text, anchors=())` gives its id. Each anchor is a
174
+ `(series, x)` pair, where `x` is a value of the column along the
175
+ bottom: a date, a time, a duration, a number, or text as llusen shows
176
+ it. A series of
177
+ `""` puts the note on every series at that place. A note's anchors
178
+ all share one series, or are all `""`.
179
+ - `set_settings(kind=None, **settings)`: the kind (`"bar"`, `"line"`,
180
+ `"doughnut"`) and the settings menu, as `smooth`, `line_fill`,
181
+ `line_from_zero`, `horizontal`, `bar_from_zero` and `pie` (each
182
+ `True` or `False`); what is left out stays. Gives whether it changed.
183
+ - `hidden_series()`, and `set_hidden_series(series)` to hide those
184
+ series from the key, as a click on it does, showing the rest. At
185
+ least one series stays shown.
186
+ - `rename(name)`, `delete()`.
187
+
188
+ ## The protocol
189
+
190
+ Each message is one line of JSON-RPC 2.0 on a TCP connection to
191
+ `127.0.0.1`, with `authenticate {token}` first. See the module notes in
192
+ `llusen-core/src/api.rs` for every method and its params, if you want to
193
+ call llusen from another language.
194
+
195
+ ## Tests
196
+
197
+ `python -m unittest discover -s tests` runs the tests against a stand-in
198
+ for llusen. `examples/orders.py` is a tour against the real thing.
llusen-0.1.0/README.md ADDED
@@ -0,0 +1,186 @@
1
+ # llusen for Python
2
+
3
+ Read and change the project open in llusen from Python: tables, formulas,
4
+ filters, summary values, group tables, charts and notes; and export them
5
+ as CSV, Excel or OpenDocument files.
6
+
7
+ ```python
8
+ import llusen
9
+
10
+ with llusen.connect() as project:
11
+ orders = project.table("orders")
12
+ orders.add_column("total", "price * qty")
13
+ orders.set_format("total", 'currency("£", 2)')
14
+ print(orders.summary())
15
+ frame = orders.to_pandas() # with pandas installed
16
+ ```
17
+
18
+ ## Setting up
19
+
20
+ - **Python 3.14** or later. The package uses only the standard library;
21
+ pandas is optional, for `to_pandas` and for giving DataFrames.
22
+ - **Install** from PyPI: `pip install llusen`, or `pip install
23
+ "llusen[pandas]"` with pandas. From this folder: `pip install .`.
24
+ - **In llusen**, choose **Tools > API Enabled**. llusen then listens on
25
+ this machine only, and writes its port and a token to `api.json` in its
26
+ state folder:
27
+ - Linux: `~/.local/state/llusen` (or `$XDG_STATE_HOME/llusen`);
28
+ - macOS: `~/Library/Application Support/llusen`;
29
+ - Windows: `%LOCALAPPDATA%\llusen`.
30
+
31
+ `llusen.connect()` reads that file and gives the token. API Enabled is
32
+ remembered, and the token is new each time it is turned on.
33
+
34
+ - **Which llusen:** each project opens in a llusen of its own, with its
35
+ own API, turned on in its own window. `connect()` reaches the llusen
36
+ used last (the one whose window last had the keyboard), as `api.json`
37
+ names it. To reach the llusen that has a given project open, name the
38
+ project's file:
39
+
40
+ ```python
41
+ with llusen.connect(project="~/reports/sales.llu") as project:
42
+ ...
43
+ ```
44
+
45
+ If no llusen has it open, or the one that has has its API off,
46
+ `NotRunning` says which. `llusen.find_project(path)` gives that
47
+ llusen's port and token without connecting. (Each llusen lists what
48
+ it has open in `instances` in the state folder.)
49
+
50
+ ## How it works
51
+
52
+ - Changes go into the project as if made in llusen. Each is one step
53
+ that Ctrl+Z takes back in that table, and the status bar says what the
54
+ script did. A table a script makes opens in a tab.
55
+ - Rows are counted in the order they were loaded or added, from 0,
56
+ whatever the user's sort and filters show. Reading gives every row.
57
+ - Reading waits until formulas are worked out, so values are never half
58
+ done. llusen gives up on a call after five minutes of waiting.
59
+ - A call llusen refuses raises `LlusenError`, with llusen's own message.
60
+ For a formula, `error.at` is the (start, end) of the problem in it.
61
+ - If llusen isn't running, or API Enabled is off, `connect` raises
62
+ `NotRunning` (a `ConnectionError`).
63
+
64
+ ## Exporting
65
+
66
+ llusen writes the file, so the path is on the machine llusen runs on. A
67
+ relative path, or one from `~`, is made whole from the script's folder
68
+ first. A file already there is replaced, through a temporary file, so a
69
+ failed write leaves it as it was. Rows go in the order llusen shows them
70
+ (sorted, every row, whatever the filters). llusen writes on a thread of
71
+ its own and answers once the file is complete; the status bar says so.
72
+ The path's extension decides the kind (`.csv`, `.xlsx`, `.ods`), unless
73
+ `format` says; a path with no extension gets the format's.
74
+
75
+ ## Values
76
+
77
+ | llusen | Python | pandas |
78
+ |----------|-------------------------------|---------------------------|
79
+ | int | `int` | `int64` (`Int64` if any are missing) |
80
+ | float | `float` | `float64` |
81
+ | bool | `bool` | `bool` (`boolean` if any are missing) |
82
+ | text | `str` | `object` |
83
+ | date | `datetime.date` | `datetime64` |
84
+ | datetime | `datetime.datetime` (UTC, aware, if the column is UTC) | `datetime64` |
85
+ | time | `datetime.time` | `object` (`datetime.time`) |
86
+ | duration | `datetime.timedelta` | `timedelta64` |
87
+ | null | `None` | missing |
88
+ | a fault such as `#DIV0` | `llusen.Fault("#DIV0")` | missing |
89
+
90
+ When giving values, NaN, `NaT` and `pd.NA` are null; numpy's numbers are
91
+ numbers; a naive datetime is sent as it is, and an aware one in UTC;
92
+ pandas' `Timedelta` and numpy's `timedelta64` are durations (to the
93
+ microsecond), and a time of day must have no time zone. A
94
+ new column's type is worked out from its values (whole numbers make
95
+ `int`, any decimal makes `float`), unless named in `types`.
96
+
97
+ ## Reference
98
+
99
+ `llusen.connect(port=None, token=None, info_file=None, timeout=360,
100
+ project=None)` gives a `Project`: with `project`, the llusen that has
101
+ that project file open. Use it in a `with` block, or call `close()`. One
102
+ connection is for one thread at a time.
103
+
104
+ **Project**
105
+ - `describe()`: every table as llusen describes it (columns, types,
106
+ formulas, formats, filters, sort, summary values, groupings, charts,
107
+ notes).
108
+ - `table_names()`, `table(name)`, `chart(name)`.
109
+ - `new_table(name, data, types=None)`: `data` is a DataFrame, or a dict
110
+ of column names to lists of values.
111
+ - `new_group_table(name, grouping)`, e.g.
112
+ `'table("orders").group(r => ({ Item: r.item }))'`.
113
+ - `new_chart(name, table, x, ys, kind=None)`: `ys` is a column or a list
114
+ of them; `kind` is `"bar"`, `"line"` or `"doughnut"` (a doughnut
115
+ takes one column of values).
116
+ - `export(path, format=None, tables=None)`: every table (or those
117
+ named) to an Excel (`.xlsx`) or OpenDocument (`.ods`) workbook, a sheet
118
+ each, with summary values at the top and column formats; charts are
119
+ left out. Gives `{"path", "rows", "sheets"}` once written.
120
+ - `call(method, **params)`: any API method, as llusen documents it.
121
+
122
+ **Table**
123
+ - `read(columns=None, start=0, count=None)`: `{name: [values]}`, read in
124
+ pieces of 100,000 rows. `to_pandas(columns=None)`.
125
+ - `columns`, `len(table)`, `summary()` (values by name), `describe()`.
126
+ - `add_column(name, formula=None, values=None, type=None)`,
127
+ `set_formula`, `remove_formula`, `rename_column`,
128
+ `delete_columns(*names)`, `set_format(column, format=None)`.
129
+ - `append_rows(data)` gives the first new row; `set_cells(column,
130
+ values, start=0)`, all or none.
131
+ - `change_case(column, case)` puts every cell of a text column in
132
+ `"upper"`, `"lower"`, `"capitalize"` (`Hello world`) or `"title"`
133
+ (`Hello World`) case, and gives how many changed. The formula
134
+ functions of the same names do it in a new column instead.
135
+ - Live rows: `with table.locked(): table.insert_rows(data)`, or
136
+ `lock()`, `insert_rows(data)` and `unlock()`. While a script has a
137
+ table locked, rows go in at its end, many times a second if need be,
138
+ with no undo steps; llusen lifts any sort and filters, and no one else
139
+ can change the table (the user watches it fill, scrolled to the end).
140
+ `insert_rows` is answered at once, with the row its first will be at,
141
+ and its rows go in as soon as the table, and the tables reading it,
142
+ have worked out the last ones. `unlock` waits until all are in. The
143
+ lock goes if the script's connection closes. `is_locked` says whether
144
+ a script has it.
145
+ - `lock(clear=False, columns=None)`: `columns` are the rows' columns and
146
+ types, as `{"price": "float", "side": "text"}`. With `clear=True`, the
147
+ table is emptied for a new run and its plain columns made exactly
148
+ those, in that order (formula columns stay, after them), with no undo
149
+ step; it gives what changed. Without, they are checked against the
150
+ table's, and a difference refuses the lock. `locked()` takes the same.
151
+ - `set_filter(column, formula=None)` gives the rows shown; `value` is the
152
+ column's cell, as in `"value > 5"`. `clear_filters()`.
153
+ - `add_summary(formula, name=None, format=None)` gives its name;
154
+ `set_summary_formula`, `set_summary_format`, `delete_summary`.
155
+ - `set_grouping(grouping)` for a group table.
156
+ - `export(path, format=None)`: as CSV, or as a workbook of this table if
157
+ the path ends in `.xlsx` or `.ods` (or `format` says so).
158
+ - `rename(name)`, `delete()`.
159
+
160
+ **Chart**
161
+ - `notes()`, `add_note(text, anchors=())` gives its id. Each anchor is a
162
+ `(series, x)` pair, where `x` is a value of the column along the
163
+ bottom: a date, a time, a duration, a number, or text as llusen shows
164
+ it. A series of
165
+ `""` puts the note on every series at that place. A note's anchors
166
+ all share one series, or are all `""`.
167
+ - `set_settings(kind=None, **settings)`: the kind (`"bar"`, `"line"`,
168
+ `"doughnut"`) and the settings menu, as `smooth`, `line_fill`,
169
+ `line_from_zero`, `horizontal`, `bar_from_zero` and `pie` (each
170
+ `True` or `False`); what is left out stays. Gives whether it changed.
171
+ - `hidden_series()`, and `set_hidden_series(series)` to hide those
172
+ series from the key, as a click on it does, showing the rest. At
173
+ least one series stays shown.
174
+ - `rename(name)`, `delete()`.
175
+
176
+ ## The protocol
177
+
178
+ Each message is one line of JSON-RPC 2.0 on a TCP connection to
179
+ `127.0.0.1`, with `authenticate {token}` first. See the module notes in
180
+ `llusen-core/src/api.rs` for every method and its params, if you want to
181
+ call llusen from another language.
182
+
183
+ ## Tests
184
+
185
+ `python -m unittest discover -s tests` runs the tests against a stand-in
186
+ for llusen. `examples/orders.py` is a tour against the real thing.
@@ -0,0 +1,40 @@
1
+ """Read and change the project open in llusen from Python.
2
+
3
+ import llusen
4
+
5
+ with llusen.connect() as project:
6
+ orders = project.table("orders")
7
+ frame = orders.to_pandas() # needs pandas
8
+ orders.add_column("total", "price * qty")
9
+
10
+ llusen must be running, with Tools > API Enabled on. The connection is
11
+ to this machine only, with the token llusen writes to its api.json: to
12
+ the llusen used last, or with `connect(project=...)` to the one that
13
+ has that project open.
14
+ """
15
+
16
+ from ._client import (
17
+ Chart,
18
+ LlusenError,
19
+ NotRunning,
20
+ Project,
21
+ Table,
22
+ connect,
23
+ find_project,
24
+ state_dir,
25
+ )
26
+ from ._values import TYPES, Fault
27
+
28
+ __all__ = [
29
+ "Chart",
30
+ "Fault",
31
+ "LlusenError",
32
+ "NotRunning",
33
+ "Project",
34
+ "TYPES",
35
+ "Table",
36
+ "connect",
37
+ "find_project",
38
+ "state_dir",
39
+ ]
40
+ __version__ = "0.1.0"