Skip to content

Commit 742c4e8

Browse files
author
Binh Vu
committed
update
1 parent b987453 commit 742c4e8

File tree

4 files changed

+95
-9
lines changed

4 files changed

+95
-9
lines changed

sand/config.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from dataclasses import dataclass
55
from functools import cached_property
66
from pathlib import Path
7-
from typing import Literal
7+
from typing import Literal, TypedDict
88

99
import serde.yaml
1010
from sm.misc.funcs import import_attr
@@ -65,7 +65,7 @@ def from_yaml(infile: Path | str) -> AppConfig:
6565
),
6666
assistant=FnConfig(
6767
default=obj["assistant"].pop("default"),
68-
funcs=obj["assistant"],
68+
funcs=AppConfig._parse_args(obj["assistant"], cwd),
6969
),
7070
export=FnConfig(default=obj["export"].pop("default"), funcs=obj["export"]),
7171
)
@@ -86,6 +86,8 @@ def _parse_args(obj: dict, cwd: Path):
8686
for k, v in obj.items():
8787
if isinstance(v, str) and v.startswith(RELPATH_CONST):
8888
out[k] = str(cwd / v[len(RELPATH_CONST) :])
89+
elif isinstance(v, dict):
90+
out[k] = AppConfig._parse_args(v, cwd)
8991
else:
9092
out[k] = v
9193
return out
@@ -97,12 +99,17 @@ class SearchConfig:
9799
ontology: str
98100

99101

102+
class FnConstructor(TypedDict):
103+
constructor: str
104+
args: dict
105+
106+
100107
@dataclass
101108
class FnConfig:
102109
default: str
103-
funcs: dict[str, str]
110+
funcs: dict[str, str | FnConstructor]
104111

105-
def get_func(self, name: Literal["default"] | str) -> str:
112+
def get_func(self, name: Literal["default"] | str) -> str | FnConstructor:
106113
if name == "default":
107114
return self.funcs[self.default]
108115
return self.funcs[name]

sand/controllers/project.py

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
1+
import tempfile
12
from dataclasses import asdict
3+
from pathlib import Path
24
from typing import List
35

46
import orjson
5-
from flask import jsonify, request
7+
from flask import jsonify, make_response, request
68
from gena import generate_api
79
from gena.deserializer import get_dataclass_deserializer
10+
from sm.dataset import Dataset, Example, FullTable
11+
from sm.prelude import I, M, O
12+
from werkzeug.exceptions import BadRequest
13+
814
from sand.controllers.helpers.upload import (
915
ALLOWED_EXTENSIONS,
1016
CSVParserOpts,
@@ -14,8 +20,10 @@
1420
parse_upload,
1521
save_upload,
1622
)
23+
from sand.controllers.table import get_friendly_fs_name
1724
from sand.models import Project
18-
from werkzeug.exceptions import BadRequest
25+
from sand.models.semantic_model import SemanticModel
26+
from sand.models.table import Table, TableRow
1927

2028
project_bp = generate_api(Project)
2129

@@ -127,3 +135,60 @@ def upload(id: int):
127135
],
128136
}
129137
)
138+
139+
140+
@project_bp.route(f"/{project_bp.name}/<id>/export", methods=["GET"])
141+
def export(id: int):
142+
"""Export tables from the project"""
143+
try:
144+
project = Project.get_by_id(id)
145+
except:
146+
raise BadRequest("Project not found")
147+
148+
examples = []
149+
for tbl in Table.select().where(Table.project == project):
150+
tblrows = list(TableRow.select().where(TableRow.table == tbl))
151+
basetbl = I.ColumnBasedTable.from_rows(
152+
records=[row.row for row in tblrows],
153+
table_id=tbl.name,
154+
headers=tbl.columns,
155+
strict=True,
156+
)
157+
table = FullTable(
158+
table=basetbl,
159+
context=(
160+
I.Context(
161+
page_title=tbl.context_page.title,
162+
page_url=tbl.context_page.url,
163+
entities=(
164+
[I.EntityId(tbl.context_page.entity, "")]
165+
if tbl.context_page.entity is not None
166+
else []
167+
),
168+
)
169+
if tbl.context_page is not None
170+
else I.Context()
171+
),
172+
links=M.Matrix.default(basetbl.shape(), list),
173+
)
174+
table.links = table.links.map_index(
175+
lambda ri, ci: tblrows[ri].links.get(ci, [])
176+
)
177+
178+
ex = Example(id=table.table.table_id, sms=[], table=table)
179+
180+
for sm in SemanticModel.select().where(SemanticModel.table == tbl):
181+
ex.sms.append(sm.data)
182+
183+
examples.append(ex)
184+
185+
with tempfile.NamedTemporaryFile(suffix=".zip") as file:
186+
Dataset(Path(file.name)).save(examples, table_fmt_indent=2)
187+
dataset = Path(file.name).read_bytes()
188+
189+
resp = make_response(dataset)
190+
resp.headers["Content-Type"] = "application/zip; charset=utf-8"
191+
resp.headers["Content-Disposition"] = (
192+
f"attachment; filename={get_friendly_fs_name(str(project.name))}.zip"
193+
)
194+
return resp

sand/helpers/service_provider.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@
22

33
from typing import Generic, Literal, TypeVar
44

5-
from sm.misc.funcs import import_func
6-
75
from sand.config import FnConfig
6+
from sm.misc.funcs import import_func
87

98
T = TypeVar("T")
109

@@ -19,7 +18,14 @@ def get_default(self) -> T:
1918

2019
def get(self, name: Literal["default"] | str) -> T:
2120
if name not in self.services:
22-
self.services[name] = import_func(self.cfg.get_func(name))()
21+
fnconstructor = self.cfg.get_func(name)
22+
if isinstance(fnconstructor, str):
23+
fn = import_func(fnconstructor)()
24+
else:
25+
fn = import_func(fnconstructor["constructor"])(
26+
**fnconstructor.get("args", {})
27+
)
28+
self.services[name] = fn
2329
return self.services[name]
2430

2531
def get_available_providers(self) -> list[str]:

sand/models/table.pyi

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from __future__ import annotations
2+
3+
class Table:
4+
id: int
5+
name: str
6+
description: str
7+
columns: list[str]
8+
project_id: int

0 commit comments

Comments
 (0)