gristcode 0.1.0__py3-none-any.whl
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.
- gristcode/__init__.py +3 -0
- gristcode/__main__.py +6 -0
- gristcode/cli.py +80 -0
- gristcode/doc.py +153 -0
- gristcode/render.py +80 -0
- gristcode-0.1.0.dist-info/METADATA +118 -0
- gristcode-0.1.0.dist-info/RECORD +11 -0
- gristcode-0.1.0.dist-info/WHEEL +5 -0
- gristcode-0.1.0.dist-info/entry_points.txt +2 -0
- gristcode-0.1.0.dist-info/licenses/LICENSE +21 -0
- gristcode-0.1.0.dist-info/top_level.txt +1 -0
gristcode/__init__.py
ADDED
gristcode/__main__.py
ADDED
gristcode/cli.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Command line entry point."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from . import __version__
|
|
8
|
+
from .doc import DocumentError, read_tables
|
|
9
|
+
from .render import render
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def build_parser():
|
|
13
|
+
parser = argparse.ArgumentParser(
|
|
14
|
+
prog="gristcode",
|
|
15
|
+
description="Render a Grist document as readable Python source, offline, "
|
|
16
|
+
"from its .grist file. The document is only ever read.",
|
|
17
|
+
)
|
|
18
|
+
parser.add_argument("document", help="path to a .grist file")
|
|
19
|
+
parser.add_argument(
|
|
20
|
+
"-t", "--table", action="append", default=[], metavar="NAME",
|
|
21
|
+
help="only this table; repeat the option for several tables",
|
|
22
|
+
)
|
|
23
|
+
parser.add_argument(
|
|
24
|
+
"--include-summary", action="store_true",
|
|
25
|
+
help="also render the summary tables Grist builds from your own tables",
|
|
26
|
+
)
|
|
27
|
+
parser.add_argument(
|
|
28
|
+
"--include-hidden", action="store_true",
|
|
29
|
+
help="also render the helper columns Grist maintains for itself",
|
|
30
|
+
)
|
|
31
|
+
parser.add_argument("--no-header", action="store_true", help="drop the two header comments")
|
|
32
|
+
parser.add_argument("--json", action="store_true", help="print the schema as JSON instead")
|
|
33
|
+
parser.add_argument("--version", action="version", version="gristcode " + __version__)
|
|
34
|
+
return parser
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def select(tables, wanted, include_summary):
|
|
38
|
+
if wanted:
|
|
39
|
+
names = set(wanted)
|
|
40
|
+
return [t for t in tables if t.table_id in names]
|
|
41
|
+
return [t for t in tables if include_summary or not t.is_summary]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def main(argv=None):
|
|
45
|
+
args = build_parser().parse_args(argv)
|
|
46
|
+
try:
|
|
47
|
+
tables = read_tables(args.document)
|
|
48
|
+
except DocumentError as exc:
|
|
49
|
+
sys.stderr.write("gristcode: %s\n" % exc)
|
|
50
|
+
return 2
|
|
51
|
+
|
|
52
|
+
chosen = select(tables, args.table, args.include_summary)
|
|
53
|
+
if args.table:
|
|
54
|
+
missing = sorted(set(args.table) - {t.table_id for t in tables})
|
|
55
|
+
if missing:
|
|
56
|
+
sys.stderr.write("gristcode: no such table: %s\n" % ", ".join(missing))
|
|
57
|
+
return 2
|
|
58
|
+
|
|
59
|
+
if args.json:
|
|
60
|
+
payload = []
|
|
61
|
+
for table in chosen:
|
|
62
|
+
entry = table.as_dict()
|
|
63
|
+
if not args.include_hidden:
|
|
64
|
+
entry["columns"] = [
|
|
65
|
+
c.as_dict() for c in table.columns if not c.is_hidden
|
|
66
|
+
]
|
|
67
|
+
payload.append(entry)
|
|
68
|
+
sys.stdout.write(json.dumps(payload, indent=2, sort_keys=True) + "\n")
|
|
69
|
+
return 0
|
|
70
|
+
|
|
71
|
+
sys.stdout.write(render(
|
|
72
|
+
chosen,
|
|
73
|
+
include_hidden=args.include_hidden,
|
|
74
|
+
header=not args.no_header,
|
|
75
|
+
))
|
|
76
|
+
return 0
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
if __name__ == "__main__":
|
|
80
|
+
sys.exit(main())
|
gristcode/doc.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Read the schema of a Grist document out of its SQLite file.
|
|
2
|
+
|
|
3
|
+
A .grist document is a SQLite database. The document's own structure lives in two metadata tables,
|
|
4
|
+
`_grist_Tables` and `_grist_Tables_column`, whose shape is defined upstream in
|
|
5
|
+
sandbox/grist/schema.py. Only those two tables are read here, and nothing is ever written.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import sqlite3
|
|
10
|
+
from urllib.parse import quote
|
|
11
|
+
|
|
12
|
+
TABLES_TABLE = "_grist_Tables"
|
|
13
|
+
COLUMNS_TABLE = "_grist_Tables_column"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DocumentError(Exception):
|
|
17
|
+
"""The file is not a readable Grist document."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Column:
|
|
21
|
+
"""One column of one user table."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, col_id, col_type, is_formula, formula, label="", description="",
|
|
24
|
+
parent_pos=0.0, reverse_col_id=None):
|
|
25
|
+
self.col_id = col_id
|
|
26
|
+
self.col_type = col_type or "Any"
|
|
27
|
+
self.is_formula = bool(is_formula)
|
|
28
|
+
self.formula = formula or ""
|
|
29
|
+
self.label = label or ""
|
|
30
|
+
self.description = description or ""
|
|
31
|
+
self.parent_pos = parent_pos or 0.0
|
|
32
|
+
self.reverse_col_id = reverse_col_id or None
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def is_hidden(self):
|
|
36
|
+
"""True for the helper columns Grist maintains for itself, which no user typed."""
|
|
37
|
+
return (
|
|
38
|
+
self.col_id == "manualSort"
|
|
39
|
+
or self.col_id.startswith("gristHelper_")
|
|
40
|
+
or self.col_id.startswith("#")
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def as_dict(self):
|
|
44
|
+
return {
|
|
45
|
+
"colId": self.col_id,
|
|
46
|
+
"type": self.col_type,
|
|
47
|
+
"isFormula": self.is_formula,
|
|
48
|
+
"formula": self.formula,
|
|
49
|
+
"label": self.label,
|
|
50
|
+
"description": self.description,
|
|
51
|
+
"reverseColId": self.reverse_col_id,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Table:
|
|
56
|
+
"""One user table of the document."""
|
|
57
|
+
|
|
58
|
+
def __init__(self, table_id, columns, summary_source=None):
|
|
59
|
+
self.table_id = table_id
|
|
60
|
+
self.columns = columns
|
|
61
|
+
self.summary_source = summary_source
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def is_summary(self):
|
|
65
|
+
return self.summary_source is not None
|
|
66
|
+
|
|
67
|
+
def as_dict(self):
|
|
68
|
+
return {
|
|
69
|
+
"tableId": self.table_id,
|
|
70
|
+
"summarySourceTable": self.summary_source,
|
|
71
|
+
"columns": [c.as_dict() for c in self.columns],
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _column_names(conn, table_name):
|
|
76
|
+
rows = conn.execute("PRAGMA table_info(%s)" % table_name).fetchall()
|
|
77
|
+
return [row[1] for row in rows]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _require_metadata(conn):
|
|
81
|
+
names = {
|
|
82
|
+
row[0]
|
|
83
|
+
for row in conn.execute("SELECT name FROM sqlite_master WHERE type = 'table'")
|
|
84
|
+
}
|
|
85
|
+
missing = [t for t in (TABLES_TABLE, COLUMNS_TABLE) if t not in names]
|
|
86
|
+
if missing:
|
|
87
|
+
raise DocumentError(
|
|
88
|
+
"this SQLite file carries no Grist document metadata (missing %s)"
|
|
89
|
+
% ", ".join(missing)
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _select(conn, table_name, wanted):
|
|
94
|
+
"""Select the wanted columns, tolerating the ones an older document does not carry."""
|
|
95
|
+
present = _column_names(conn, table_name)
|
|
96
|
+
used = [name for name in wanted if name in present]
|
|
97
|
+
rows = conn.execute("SELECT %s FROM %s" % (", ".join(used), table_name)).fetchall()
|
|
98
|
+
return [dict(zip(used, row)) for row in rows]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def read_tables(path):
|
|
102
|
+
"""Return the user tables of the document at `path`, ordered by table id.
|
|
103
|
+
|
|
104
|
+
Columns are ordered the way the document orders them, data columns before formula columns.
|
|
105
|
+
"""
|
|
106
|
+
# The path is percent encoded before it goes into the URI, so that a name holding `?` or `#`
|
|
107
|
+
# cannot end the query string early and drop the read only mode.
|
|
108
|
+
uri = "file:%s?mode=ro" % quote(os.path.abspath(path))
|
|
109
|
+
try:
|
|
110
|
+
conn = sqlite3.connect(uri, uri=True)
|
|
111
|
+
except sqlite3.Error as exc:
|
|
112
|
+
raise DocumentError("cannot open %s: %s" % (path, exc))
|
|
113
|
+
try:
|
|
114
|
+
try:
|
|
115
|
+
_require_metadata(conn)
|
|
116
|
+
table_rows = _select(conn, TABLES_TABLE, ["id", "tableId", "summarySourceTable"])
|
|
117
|
+
column_rows = _select(conn, COLUMNS_TABLE, [
|
|
118
|
+
"id", "parentId", "parentPos", "colId", "type", "isFormula", "formula",
|
|
119
|
+
"label", "description", "reverseCol",
|
|
120
|
+
])
|
|
121
|
+
except sqlite3.DatabaseError as exc:
|
|
122
|
+
raise DocumentError("cannot read %s: %s" % (path, exc))
|
|
123
|
+
finally:
|
|
124
|
+
conn.close()
|
|
125
|
+
|
|
126
|
+
table_id_by_ref = {row["id"]: row["tableId"] for row in table_rows}
|
|
127
|
+
col_id_by_ref = {row["id"]: row.get("colId") for row in column_rows}
|
|
128
|
+
|
|
129
|
+
by_parent = {}
|
|
130
|
+
for row in column_rows:
|
|
131
|
+
column = Column(
|
|
132
|
+
col_id=row.get("colId") or "",
|
|
133
|
+
col_type=row.get("type"),
|
|
134
|
+
is_formula=row.get("isFormula"),
|
|
135
|
+
formula=row.get("formula"),
|
|
136
|
+
label=row.get("label"),
|
|
137
|
+
description=row.get("description"),
|
|
138
|
+
parent_pos=row.get("parentPos"),
|
|
139
|
+
reverse_col_id=col_id_by_ref.get(row.get("reverseCol")),
|
|
140
|
+
)
|
|
141
|
+
by_parent.setdefault(row.get("parentId"), []).append(column)
|
|
142
|
+
|
|
143
|
+
tables = []
|
|
144
|
+
for row in table_rows:
|
|
145
|
+
columns = by_parent.get(row["id"], [])
|
|
146
|
+
columns.sort(key=lambda c: (c.is_formula, c.parent_pos, c.col_id))
|
|
147
|
+
tables.append(Table(
|
|
148
|
+
table_id=row["tableId"],
|
|
149
|
+
columns=columns,
|
|
150
|
+
summary_source=table_id_by_ref.get(row.get("summarySourceTable")),
|
|
151
|
+
))
|
|
152
|
+
tables.sort(key=lambda t: t.table_id)
|
|
153
|
+
return tables
|
gristcode/render.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Turn the tables of a document into Python source text.
|
|
2
|
+
|
|
3
|
+
The shape follows the class-per-table rendering the Grist sandbox uses (upstream
|
|
4
|
+
sandbox/grist/gencode.py). Formula bodies are printed exactly as the document stores them, which
|
|
5
|
+
is how they were typed; the sandbox rewrites them before running them, and that rewriting is not
|
|
6
|
+
done here.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
INDENT = " "
|
|
10
|
+
|
|
11
|
+
HEADER = (
|
|
12
|
+
"# Rendered by gristcode. Formula bodies are shown exactly as stored in the document,\n"
|
|
13
|
+
"# so this text is meant to be read and compared, not executed.\n"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def grist_type(col_type, reverse_col_id=None):
|
|
18
|
+
"""Return the Grist type expression for a column type string, e.g. `grist.Reference('People')`."""
|
|
19
|
+
head, _, tail = (col_type or "").partition(":")
|
|
20
|
+
name = head.strip() or "Any"
|
|
21
|
+
if name == "Ref":
|
|
22
|
+
name = "Reference"
|
|
23
|
+
elif name == "RefList":
|
|
24
|
+
name = "ReferenceList"
|
|
25
|
+
args = []
|
|
26
|
+
tail = tail.strip().replace("'", "\\'")
|
|
27
|
+
if tail:
|
|
28
|
+
args.append("'%s'" % tail)
|
|
29
|
+
if reverse_col_id and name in ("Reference", "ReferenceList"):
|
|
30
|
+
args.append("reverse_of=%r" % reverse_col_id)
|
|
31
|
+
return "grist.%s(%s)" % (name, ", ".join(args))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _block(text, indent):
|
|
35
|
+
"""Indent a formula body, giving `pass` when the document holds no text at all."""
|
|
36
|
+
lines = (text or "").replace("\r\n", "\n").replace("\r", "\n").split("\n")
|
|
37
|
+
while lines and not lines[-1].strip():
|
|
38
|
+
lines.pop()
|
|
39
|
+
if not lines:
|
|
40
|
+
return [indent + "pass"]
|
|
41
|
+
return [(indent + line).rstrip() for line in lines]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def render_column(column, indent=INDENT):
|
|
45
|
+
"""Return the lines rendering one column."""
|
|
46
|
+
type_expr = grist_type(column.col_type, column.reverse_col_id)
|
|
47
|
+
if column.is_formula:
|
|
48
|
+
lines = [""]
|
|
49
|
+
if column.col_type != "Any":
|
|
50
|
+
lines.append("%s@grist.formulaType(%s)" % (indent, type_expr))
|
|
51
|
+
lines.append("%sdef %s(rec, table):" % (indent, column.col_id))
|
|
52
|
+
lines.extend(_block(column.formula, indent + INDENT))
|
|
53
|
+
return lines
|
|
54
|
+
lines = []
|
|
55
|
+
if column.formula:
|
|
56
|
+
lines.append("%s# trigger formula:" % indent)
|
|
57
|
+
lines.extend("%s# %s" % (indent, line) for line in _block(column.formula, ""))
|
|
58
|
+
lines.append("%s%s = %s" % (indent, column.col_id, type_expr))
|
|
59
|
+
return lines
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def render_table(table, include_hidden=False):
|
|
63
|
+
"""Return the lines rendering one table as a class."""
|
|
64
|
+
lines = ["@grist.UserTable", "class %s:" % table.table_id]
|
|
65
|
+
if table.is_summary:
|
|
66
|
+
lines.append("%s_summarySourceTable = %r" % (INDENT, table.summary_source))
|
|
67
|
+
columns = [c for c in table.columns if include_hidden or not c.is_hidden]
|
|
68
|
+
for column in columns:
|
|
69
|
+
lines.extend(render_column(column))
|
|
70
|
+
if len(lines) == 2:
|
|
71
|
+
lines.append(INDENT + "pass")
|
|
72
|
+
return lines
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def render(tables, include_hidden=False, header=True):
|
|
76
|
+
"""Return the whole document as one block of Python source text."""
|
|
77
|
+
parts = [HEADER] if header else []
|
|
78
|
+
for table in tables:
|
|
79
|
+
parts.append("\n".join(render_table(table, include_hidden=include_hidden)) + "\n")
|
|
80
|
+
return "\n\n".join(parts)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gristcode
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Render a Grist document as readable Python source, offline, straight from the .grist file. Standard library only.
|
|
5
|
+
Author: Younes Z.
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Rezarys/gristcode
|
|
8
|
+
Project-URL: Issues, https://github.com/Rezarys/gristcode/issues
|
|
9
|
+
Keywords: grist,spreadsheet,schema,formulas,sqlite,code view,documentation,self hosted
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Database
|
|
17
|
+
Classifier: Topic :: Office/Business :: Financial :: Spreadsheet
|
|
18
|
+
Classifier: Topic :: Utilities
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# gristcode
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
pip install gristcode
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Not affiliated with Grist Labs. "Grist" is used here only to say which file this tool reads.
|
|
31
|
+
|
|
32
|
+
Print a Grist document as readable Python source, offline, straight from the `.grist` file, so you can read it in your editor, diff it, and commit it.
|
|
33
|
+
|
|
34
|
+
Grist already shows a rendering of your document as Python in its Code view panel, but there is no way to get that text out of the browser other than selecting the page, pasting it somewhere, and deleting everything that came along with it. `gristcode` gives you the same shape of text on standard output, from the file, with nothing to trim.
|
|
35
|
+
|
|
36
|
+
## Use
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
gristcode mydoc.grist
|
|
40
|
+
gristcode mydoc.grist --table People --table Orders
|
|
41
|
+
gristcode mydoc.grist --json
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
A `.grist` file is what the Download button gives you in the document menu, and it is also the file a self-hosted instance keeps on disk. The document is opened read only and is never written to.
|
|
45
|
+
|
|
46
|
+
Output on a small document with two tables:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
# Rendered by gristcode. Formula bodies are shown exactly as stored in the document,
|
|
50
|
+
# so this text is meant to be read and compared, not executed.
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@grist.UserTable
|
|
54
|
+
class Orders:
|
|
55
|
+
Customer = grist.Reference('People')
|
|
56
|
+
Amount = grist.Numeric()
|
|
57
|
+
# trigger formula:
|
|
58
|
+
# NOW()
|
|
59
|
+
Placed = grist.DateTime('UTC')
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@grist.UserTable
|
|
63
|
+
class People:
|
|
64
|
+
Name = grist.Text()
|
|
65
|
+
Age = grist.Int()
|
|
66
|
+
|
|
67
|
+
@grist.formulaType(grist.Text())
|
|
68
|
+
def Greeting(rec, table):
|
|
69
|
+
return "Hi " + $Name
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Options
|
|
73
|
+
|
|
74
|
+
- `-t NAME`, `--table NAME`: render only this table, and repeat the option for several tables. Naming a summary table explicitly works too.
|
|
75
|
+
- `--include-summary`: also render the summary tables Grist builds from your own tables. They are left out by default.
|
|
76
|
+
- `--include-hidden`: also render the helper columns Grist maintains for itself. The rule used here is that a column is a helper when its identifier is `manualSort`, or starts with `gristHelper_`, or starts with `#`.
|
|
77
|
+
- `--no-header`: drop the two header comment lines, which is handy when the output goes into version control.
|
|
78
|
+
- `--json`: print the schema as JSON instead of Python, with one entry per table and the column identifier, type, formula, label and description of each column.
|
|
79
|
+
- `--version`: print the version and exit.
|
|
80
|
+
|
|
81
|
+
Exit code is 0 on success and 2 when the file cannot be read as a Grist document or when a table named with `--table` does not exist.
|
|
82
|
+
|
|
83
|
+
## What this is not
|
|
84
|
+
|
|
85
|
+
The Code view panel inside Grist shows formula bodies **after** the sandbox has rewritten them, which is where `$field` becomes `rec.field` and a `return` gets inserted. `gristcode` does not do that rewriting: it prints each formula exactly as the document stores it, which is how you typed it. The output is therefore made to be read and compared, not executed, and the sample above shows `$Name` for that reason.
|
|
86
|
+
|
|
87
|
+
Only the two metadata tables `_grist_Tables` and `_grist_Tables_column` are read. Your data rows, your attachments and your access rules are never opened and never printed.
|
|
88
|
+
|
|
89
|
+
## Honest limits
|
|
90
|
+
|
|
91
|
+
This tool has never been run against a real Grist document. It is tested against small SQLite files written by its own test suite, shaped after the document metadata schema published in the Grist source tree. If you run it on a real document and something comes out wrong, please open an issue and paste what you got.
|
|
92
|
+
|
|
93
|
+
The shape of the output follows the schema as of Grist schema version 46. A document saved by a much older or much newer version may carry columns this tool does not know about, and those are ignored rather than guessed at.
|
|
94
|
+
|
|
95
|
+
## Install from source
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
git clone https://github.com/Rezarys/gristcode
|
|
99
|
+
pip install ./gristcode
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Python 3.9 or newer. No dependencies outside the standard library.
|
|
103
|
+
|
|
104
|
+
Tests, once the package is installed:
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
python -m unittest discover -s tests -t tests
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Background
|
|
111
|
+
|
|
112
|
+
Built after a request on the Grist issue tracker for an easier way to get the document as code out of the Code view panel: https://github.com/gristlabs/grist-core/issues/2574
|
|
113
|
+
|
|
114
|
+
Built with AI assistance, reviewed and tested by me.
|
|
115
|
+
|
|
116
|
+
## License
|
|
117
|
+
|
|
118
|
+
MIT. Younes Z.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
gristcode/__init__.py,sha256=jm3AOHCwZdG7rMA_txwXmyvUe0wvrY2GKpTG5tvs5bw,111
|
|
2
|
+
gristcode/__main__.py,sha256=4JMK66Wj4uLZTKbF-sT3LAxOsr6buig77PmOkJCRRxw,83
|
|
3
|
+
gristcode/cli.py,sha256=Z41JB2q7mGq8A5upZka-rSZEdiI2Iy06ji5N89h7pxs,2593
|
|
4
|
+
gristcode/doc.py,sha256=_b8ZYGH6LqFdbjZYbc4dxlP8lBYtyG51b9KLlArj9T4,5307
|
|
5
|
+
gristcode/render.py,sha256=aavsbAL8t-aYb5xtGWvqg3dG3LpZB8KGtaSv3nAqaDU,3090
|
|
6
|
+
gristcode-0.1.0.dist-info/licenses/LICENSE,sha256=Y2A2qHmmnYqakqTX3wPyhl2UBUwP8eCtG7yH1ylrWRI,1066
|
|
7
|
+
gristcode-0.1.0.dist-info/METADATA,sha256=DpA0Bi28rjwbHszS_mWnckOUhXXKU5VfhVRuJwaU8y0,5056
|
|
8
|
+
gristcode-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
9
|
+
gristcode-0.1.0.dist-info/entry_points.txt,sha256=CSkt7lzAuX8UNcTmSExbB78HE-IIkGqSSo1fNlwS9sM,49
|
|
10
|
+
gristcode-0.1.0.dist-info/top_level.txt,sha256=3j6MDB3FMwnZhLZe5ikKBiertzv3CZHBOI3eSkrYFp4,10
|
|
11
|
+
gristcode-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Younes Z.
|
|
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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
gristcode
|