Skip to content

Commit 8e52765

Browse files
Merge pull request #392 from maxfordham/pydantic-model-creation-update-from-edittsv
Updated pydantic model generation in edittsv lifecycle
2 parents 0cb0fb3 + 72ecf08 commit 8e52765

12 files changed

Lines changed: 87 additions & 97 deletions

docs/TestArrayTransposed.xlsx

7.59 KB
Binary file not shown.

docs/digital-schedules-issue.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
from ipyautoui.automapschema import pydantic_model_from_json_schema
2-
from ipyautoui.automapschema import pydantic_model_file_from_json_schema
1+
from ipyautoui._utils import pydantic_model_from_json_schema
2+
from ipyautoui._utils import pydantic_model_file_from_json_schema
33
from ipyautoui.custom.edittsv import EditTsvWithDiff
44
from ipyautoui.custom.edittsv_with_diff_and_key_mapping import EditTsvWithDiffAndKeyMapping
55
import pathlib

docs/digital-schedules-phantom-changes-issue.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
from ipyautoui.automapschema import pydantic_model_from_json_schema
2-
from ipyautoui.automapschema import pydantic_model_file_from_json_schema
1+
from ipyautoui._utils import pydantic_model_from_json_schema
2+
from ipyautoui._utils import pydantic_model_file_from_json_schema
33
from ipyautoui.custom.edittsv import EditTsvWithDiff
44
import pathlib
55
import numpy as np

docs/editgrid.qmd

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,24 +21,24 @@ These docs requires a python kernel to run. Try on Binder [![Binder](https://myb
2121

2222
```{python}
2323
from ipyautoui.custom.editgrid import EditGrid
24+
from ipyautoui.custom.edittsv import EditTsvFileUpload
2425
import typing as ty
2526
from pydantic import BaseModel, Field, RootModel
2627
from ipyautoui import AutoUi
2728
2829
2930
class DataFrameCols(BaseModel):
30-
string: str = Field("string", json_schema_extra=dict(column_width=100))
31-
integer: int = Field(1, json_schema_extra=dict(column_width=80))
32-
floater: float = Field(3.1415, json_schema_extra=dict(column_width=70, global_decimal_places=3))
33-
something_else: float = Field(324, json_schema_extra=dict(column_width=100))
34-
nullable_string: ty.Optional[str] = None
31+
string: str = Field("string", json_schema_extra=dict(column_width=100, name="string"))
32+
integer: int = Field(1, json_schema_extra=dict(column_width=80, name="integer"))
33+
floater: float = Field(3.1415, json_schema_extra=dict(column_width=70, global_decimal_places=3, name="floater"))
34+
something_else: float = Field(324, json_schema_extra=dict(column_width=100, name="something_else"))
3535
3636
class TestDataFrame(RootModel):
3737
"""a description of TestDataFrame"""
3838
39-
root: ty.List[DataFrameCols] = Field(json_schema_extra=dict(format="dataframe"))
39+
root: ty.List[DataFrameCols] = Field(json_schema_extra=dict(format="dataframe", datagrid_index_name=("name", )))
4040
41-
egrid = EditGrid(schema=TestDataFrame, show_ui_io=True)
41+
egrid = EditGrid(schema=TestDataFrame, show_ui_io=True, ui_io=EditTsvFileUpload)
4242
egrid
4343
```
4444

src/ipyautoui/_utils.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@
2020
from markdown import markdown
2121
from math import log10, floor
2222
from pydantic import BaseModel, field_validator, Field, ValidationInfo
23+
from datamodel_code_generator import DataModelType, InputFileType, generate
24+
from tempfile import TemporaryDirectory
2325
from IPython.display import display, Markdown
26+
import sys
2427

2528
logger = logging.getLogger(__name__)
2629
frozenmap = immutables.Map
@@ -527,3 +530,46 @@ def is_null(v) -> bool:
527530
return False
528531
else:
529532
return pd.isnull(v)
533+
534+
def pydantic_model_file_from_json_schema(json_schema, fpth):
535+
return generate(
536+
json.dumps(json_schema, ensure_ascii=False),
537+
input_file_type=InputFileType.JsonSchema,
538+
input_filename="example.json",
539+
output=fpth,
540+
output_model_type=DataModelType.PydanticV2BaseModel,
541+
capitalise_enum_members=True,
542+
field_include_all_keys=True
543+
)
544+
545+
def pydantic_model_from_json_schema(json_schema: dict) -> ty.Type[BaseModel]:
546+
load = json_schema["title"].replace(" ", "") if "title" in json_schema else "Model"
547+
548+
with TemporaryDirectory() as temporary_directory_name:
549+
temporary_directory = pathlib.Path(temporary_directory_name)
550+
file_path = "model.py"
551+
module_name = file_path.split(".")[0]
552+
output = pathlib.Path(temporary_directory / file_path)
553+
554+
pydantic_model_file_from_json_schema(json_schema, output)
555+
556+
#HACK refer to https://github.com/koxudaxi/datamodel-code-generator/issues/2534 for official fix, then remove the PATCH LOGIC once that is resolved
557+
# --- NEW PATCH LOGIC ---
558+
if json_schema.get("title") == "Project Building Area":
559+
text = output.read_text()
560+
561+
# Replace Enum → IntEnum in TargetYear only
562+
text = text.replace("class TargetYear(Enum):", "class TargetYear(IntEnum):")
563+
564+
# Ensure IntEnum is imported
565+
if "from enum import IntEnum" not in text:
566+
text = text.replace("from enum import Enum", "from enum import Enum, IntEnum")
567+
568+
output.write_text(text)
569+
# --- END PATCH LOGIC ---
570+
571+
spec = importlib.util.spec_from_file_location(module_name, output)
572+
module = importlib.util.module_from_spec(spec)
573+
sys.modules[module_name] = module
574+
spec.loader.exec_module(module)
575+
return getattr(module, load)

src/ipyautoui/automapschema.py

Lines changed: 4 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -12,62 +12,13 @@
1212
from ipyautoui.custom.filechooser import FileChooser
1313
from ipyautoui.custom.date_string import DatePickerString, NaiveDatetimePickerString
1414
from ipyautoui.autobox import AutoBox
15-
from tempfile import TemporaryDirectory
16-
import pathlib
17-
from datamodel_code_generator import DataModelType, InputFileType, generate
18-
import json
19-
import importlib.util
20-
import sys
2115

2216
import logging
2317

2418
logger = logging.getLogger(__name__)
2519

26-
def pydantic_model_file_from_json_schema(json_schema, fpth):
27-
return generate(
28-
json.dumps(json_schema, ensure_ascii=False),
29-
input_file_type=InputFileType.JsonSchema,
30-
input_filename="example.json",
31-
output=fpth,
32-
output_model_type=DataModelType.PydanticV2BaseModel,
33-
capitalise_enum_members=True,
34-
field_include_all_keys=True
35-
)
36-
37-
def pydantic_model_from_json_schema(json_schema: dict) -> ty.Type[BaseModel]:
38-
load = json_schema["title"].replace(" ", "") if "title" in json_schema else "Model"
39-
40-
with TemporaryDirectory() as temporary_directory_name:
41-
temporary_directory = pathlib.Path(temporary_directory_name)
42-
file_path = "model.py"
43-
module_name = file_path.split(".")[0]
44-
output = pathlib.Path(temporary_directory / file_path)
45-
46-
pydantic_model_file_from_json_schema(json_schema, output)
47-
48-
#HACK refer to https://github.com/koxudaxi/datamodel-code-generator/issues/2534 for official fix, then remove the PATCH LOGIC once that is resolved
49-
# --- NEW PATCH LOGIC ---
50-
if json_schema.get("title") == "Project Building Area":
51-
text = output.read_text()
52-
53-
# Replace Enum → IntEnum in TargetYear only
54-
text = text.replace("class TargetYear(Enum):", "class TargetYear(IntEnum):")
55-
56-
# Ensure IntEnum is imported
57-
if "from enum import IntEnum" not in text:
58-
text = text.replace("from enum import Enum", "from enum import Enum, IntEnum")
59-
60-
output.write_text(text)
61-
# --- END PATCH LOGIC ---
62-
63-
spec = importlib.util.spec_from_file_location(module_name, output)
64-
module = importlib.util.module_from_spec(spec)
65-
sys.modules[module_name] = module
66-
spec.loader.exec_module(module)
67-
return getattr(module, load)
68-
6920
def _init_model_schema(
70-
schema=None, by_alias=False, generate_pydantic_model_from_json_schema = True
21+
schema=None, by_alias=False
7122
) -> tuple[ty.Optional[ty.Type[BaseModel]], dict]:
7223
if schema is None:
7324
return None, {
@@ -76,17 +27,9 @@ def _init_model_schema(
7627
"items": {"properties": {}},
7728
}
7829
if isinstance(schema, dict):
79-
if generate_pydantic_model_from_json_schema:
80-
schema = replace_refs(schema, merge_props=True)
81-
schema = {k: v for k, v in schema.items() if k != "$defs"}
82-
model = pydantic_model_from_json_schema(schema)
83-
else:
84-
model = None
85-
schema = replace_refs(schema, merge_props=True)
86-
schema = {k: v for k, v in schema.items() if k != "$defs"}
87-
# IDEA: Possible implementations -@jovyan at 8/24/2022, 12:05:02 PM
88-
# jsonschema_to_pydantic
89-
# https://koxudaxi.github.io/datamodel-code-generator/using_as_module/
30+
model = None
31+
schema = replace_refs(schema, merge_props=True)
32+
schema = {k: v for k, v in schema.items() if k != "$defs"}
9033
else:
9134
model = schema # the "model" passed is a pydantic model
9235
schema = model.model_json_schema(by_alias=by_alias).copy()

src/ipyautoui/custom/autogrid.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -514,7 +514,6 @@ def update_from_schema(
514514
by_alias: bool = False,
515515
by_title: bool = True,
516516
order: ty.Optional[tuple] = None,
517-
generate_pydantic_model_from_json_schema: bool = False,
518517
**kwargs,
519518
):
520519
self.__init__(
@@ -523,7 +522,6 @@ def update_from_schema(
523522
by_alias=by_alias,
524523
by_title=by_title,
525524
order=order,
526-
generate_pydantic_model_from_json_schema=generate_pydantic_model_from_json_schema,
527525
**kwargs,
528526
)
529527

@@ -606,7 +604,6 @@ def __init__(
606604
by_alias: bool = False,
607605
by_title: bool = True,
608606
order: ty.Optional[tuple] = None,
609-
generate_pydantic_model_from_json_schema: bool = False,
610607
**kwargs,
611608
):
612609
# accept schema or pydantic schema
@@ -615,7 +612,7 @@ def __init__(
615612
)
616613
self.by_title = by_title
617614
self.selection_mode = MAP_TRANSPOSED_SELECTION_MODE[self.transposed]
618-
self.model, self.schema = asch._init_model_schema(schema, by_alias=by_alias, generate_pydantic_model_from_json_schema=generate_pydantic_model_from_json_schema)
615+
self.model, self.schema = asch._init_model_schema(schema, by_alias=by_alias)
619616
# ^ generates gridschema
620617
self.gridschema.get_traits = self.datagrid_trait_names
621618
_data = self._init_data(data)

src/ipyautoui/custom/editgrid.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,6 @@ def __init__(
289289
title: str = None,
290290
description: str = None,
291291
show_title: bool = True,
292-
generate_pydantic_model_from_json_schema: bool = False,
293292
show_ui_io: bool = False,
294293
**kwargs,
295294
): # TODO: use **kwargs to pass attributes to EditGrid as in AutoObject and AutoArray
@@ -302,7 +301,6 @@ def __init__(
302301
self.by_title = by_title
303302
self.by_alias = by_alias
304303
self.datahandler = datahandler
305-
self.generate_pydantic_model_from_json_schema = generate_pydantic_model_from_json_schema
306304

307305
self.ui_io = None
308306
self._ui_io_factory = None
@@ -346,7 +344,7 @@ def update_from_schema(
346344
):
347345
value = None if value is None or value == [{}] else pd.DataFrame(value)
348346
self.grid.update_from_schema(
349-
schema, data=value, by_alias=self.by_alias, generate_pydantic_model_from_json_schema=self.generate_pydantic_model_from_json_schema, **kwargs
347+
schema, data=value, by_alias=self.by_alias, **kwargs
350348
)
351349
self._init_ui_callables(
352350
ui_add=ui_add, ui_edit=ui_edit, ui_delete=ui_delete, ui_copy=ui_copy, ui_io=ui_io
@@ -367,7 +365,7 @@ def _init_autogrid(
367365
None if value is None or value == [{}] else pd.DataFrame(value)
368366
)
369367
self.grid = AutoGrid(
370-
schema, data=getvalue(value), generate_pydantic_model_from_json_schema=self.generate_pydantic_model_from_json_schema, by_alias=self.by_alias, **kwargs
368+
schema, data=getvalue(value), by_alias=self.by_alias, **kwargs
371369
)
372370

373371
def _init_ui_callables(
@@ -403,24 +401,24 @@ def _missing_model_ui():
403401

404402
if ui_io is None:
405403
def _factory():
406-
if self.model is None:
404+
if self.schema is None:
407405
return _missing_model_ui()
408406
return EditTsvWithDiff(
409-
model=self.model, fn_upload=self.fn_upload, transposed=self.transposed, by_alias = self.by_alias
407+
schema=self.schema, fn_upload=self.fn_upload, transposed=self.transposed, by_alias = self.by_alias
410408
)
411409
self._ui_io_factory = _factory
412410
else:
413411
def _factory_custom():
414-
if self.model is None:
412+
if self.schema is None:
415413
return _missing_model_ui()
416414
try:
417415
return ui_io(
418-
model=self.model, fn_upload=self.fn_upload, transposed=self.transposed, by_alias = self.by_alias
416+
schema=self.schema, fn_upload=self.fn_upload, transposed=self.transposed, by_alias = self.by_alias
419417
)
420418
except Exception as e:
421419
raise RuntimeError(
422420
f"Failed to initialize ui_io '{ui_io.__name__}'."
423-
" Required traits are: `model`, `fn_upload`, `transposed`."
421+
" Required traits are: `schema`, `fn_upload`, `transposed`."
424422
f" Original error: {e}"
425423
) from e
426424
self._ui_io_factory = _factory_custom
@@ -430,7 +428,7 @@ def _factory_custom():
430428
def _init_ui_io(self, ui_io):
431429
if ui_io is not None and self.ui_io_initialised:
432430
self.ui_io = ui_io(
433-
model=self.model, fn_upload=self.fn_upload, transposed=self.transposed, by_alias = self.by_alias
431+
schema=self.schema, fn_upload=self.fn_upload, transposed=self.transposed, by_alias = self.by_alias
434432
)
435433

436434
def _ensure_ui_io_initialised(self):

src/ipyautoui/custom/editgridfile.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
EditGrid,
33
DataHandler,
44
)
5-
from ipyautoui.custom.edittsv import Changes
5+
from ipyautoui.custom.edittsv import Changes, EditTsvFileUpload
66
import pathlib
77
import traitlets as tr
88
import json
@@ -183,21 +183,21 @@ def handle_crud(self, changes: Changes):
183183
AUTO_GRID_DEFAULT_VALUE = data
184184

185185
class DataFrameCols(BaseModel):
186-
id: int = Field(1, json_schema_extra=dict(column_width=80, section="a"))
186+
id: int = Field(1, json_schema_extra=dict(column_width=80, section="a", name= "id"))
187187
string: str = Field(
188-
"string", json_schema_extra=dict(column_width=400, section="a")
188+
"string", json_schema_extra=dict(column_width=400, section="a", name= "string")
189189
)
190-
integer: int = Field(1, json_schema_extra=dict(column_width=80, section="a"))
190+
integer: int = Field(1, json_schema_extra=dict(column_width=80, section="a", name= "integer"))
191191
floater: float = Field(
192-
None, json_schema_extra=dict(column_width=70, section="b")
192+
None, json_schema_extra=dict(column_width=70, section="b", name= "floater")
193193
)
194194

195195
class TestDataFrame(RootModel):
196196
"""a description of TestDataFrame"""
197197
root: ty.List[DataFrameCols] = Field(
198198
default=AUTO_GRID_DEFAULT_VALUE,
199199
json_schema_extra=dict(
200-
format="dataframe", datagrid_index_name=("section", "title")
200+
format="dataframe", datagrid_index_name=("section", "title", "name")
201201
),
202202
)
203203

@@ -214,7 +214,8 @@ class TestDataFrame(RootModel):
214214
global_decimal_places=1,
215215
column_width={"String": 400},
216216
fpth=json_path,
217-
show_ui_io=True
217+
show_ui_io=True,
218+
ui_io=EditTsvFileUpload
218219
)
219220
display(edit_grid_file)
220221

@@ -223,4 +224,3 @@ class TestDataFrame(RootModel):
223224

224225
# -
225226

226-

src/ipyautoui/custom/edittsv.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@
1111
from IPython.display import clear_output
1212
from deepdiff import DeepDiff
1313
from deepdiff.helper import COLORED_COMPACT_VIEW # COLORED_VIEW,
14+
from ipyautoui.automapschema import _init_model_schema
1415
from ipyautoui.custom.filedownload import MakeFileAndDownload
1516
from ipyautoui.custom.fileupload import TempFileUploadProcessor
1617
from copy import deepcopy
1718
import typing as ty
1819
import xlsxdatagrid as xdg
1920
from pathlib import Path
2021
from ipyautoui.watch_validate import pydantic_validate
22+
from ipyautoui._utils import pydantic_model_from_json_schema
2123

2224
from ipyautoui.constants import BUTTON_WIDTH_MIN
2325

@@ -132,7 +134,7 @@ def default_fn_upload(value):
132134

133135
class EditTsv(CopyToClipboard):
134136
_value = tr.List(value=None, trait=tr.Dict, allow_none=True)
135-
model = tr.Type(klass=BaseModel)
137+
model = tr.Type(klass=BaseModel, default_value=None, allow_none=True)
136138
by_alias = tr.Bool(default_value=False)
137139
errors = tr.List(value=[], trait=tr.Dict)
138140
fn_upload = tr.Callable(default_value=default_fn_upload)
@@ -145,6 +147,7 @@ class EditTsv(CopyToClipboard):
145147
header_depth = tr.Int(default_value=1)
146148
disable_text_editing = tr.Bool(default_value=True)
147149
filename_suffix = tr.Unicode(default_value="", allow_none=True)
150+
schema = tr.Dict()
148151

149152
@tr.observe("upload_status")
150153
def upload_status_onchange(self, on_change):
@@ -195,6 +198,10 @@ def disable_text_editing_onchange(self, on_change):
195198
self.text.disabled = False
196199

197200
def __init__(self, **kwargs):
201+
#If model json schema is given, generate model from it, otherwise use given model
202+
self.model, self.schema = _init_model_schema(kwargs.get("schema"))
203+
if self.model is None:
204+
self.model = pydantic_model_from_json_schema(self.schema)
198205
self.vbx_errors = w.VBox()
199206
self.bn_upload_text = w.Button(
200207
icon="save", disabled=True, layout={"width": BUTTON_WIDTH_MIN}

0 commit comments

Comments
 (0)