Skip to content

Commit

Permalink
fix 2 issues: with NOW() and dataclasses field
Browse files Browse the repository at this point in the history
  • Loading branch information
xnuinside committed Jul 12, 2021
1 parent d24bfa6 commit e8fbb1a
Show file tree
Hide file tree
Showing 15 changed files with 482 additions and 267 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
**v0.8.2**
1. Added fundamental concept of TableMetaModel - class that unifies metadata parsed from different classes/ORM models types/DDLs to one standard to allow easy way convert one models to another
in next releases it will be used for converter from one type of models to another.
2. Fixed issue: https://github.com/xnuinside/omymodels/issues/18 "NOW() not recognized as now()"
3. Fixed issue: https://github.com/xnuinside/omymodels/issues/19 "Default value of now() always returns same time, use field for dataclass"

**v0.8.1**
1. Parser version is updated (fixed several issues with generation)
2. Fixed issue with Unique Constraint after schema in SQLAlchemy Core
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ And result will be this:

1. Add Sequence generation in Models (Gino, SQLAlchemy)
2. Generate Tortoise ORM models (https://tortoise-orm.readthedocs.io/en/latest/)
3. Convert SQLAlchemy models to DjangoORM, Pydantic, SQLAlchemy Tables, Dataclasses (?)

## How to contribute

Expand All @@ -277,6 +278,12 @@ Please describe issue that you want to solve and open the PR, I will review it a
Any questions? Ping me in Telegram: https://t.me/xnuinside

## Changelog
**v0.8.2**
1. Added fundamental concept of TableMetaModel - class that unifies metadata parsed from different classes/ORM models types/DDLs to one standard to allow easy way convert one models to another
in next releases it will be used for converter from one type of models to another.
2. Fixed issue: https://github.com/xnuinside/omymodels/issues/18 "NOW() not recognized as now()"
3. Fixed issue: https://github.com/xnuinside/omymodels/issues/19 "Default value of now() always returns same time, use field for dataclass"

**v0.8.1**
1. Parser version is updated (fixed several issues with generation)
2. Fixed issue with Unique Constraint after schema in SQLAlchemy Core
Expand Down
9 changes: 9 additions & 0 deletions docs/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ TODO in next releases

#. Add Sequence generation in Models (Gino, SQLAlchemy)
#. Generate Tortoise ORM models (https://tortoise-orm.readthedocs.io/en/latest/)
#. Convert SQLAlchemy models to DjangoORM, Pydantic, SQLAlchemy Tables, Dataclasses (?)

How to contribute
-----------------
Expand All @@ -292,6 +293,14 @@ Any questions? Ping me in Telegram: https://t.me/xnuinside
Changelog
---------

**v0.8.2**


#. Added fundamental concept of TableMetaModel - class that unifies metadata parsed from different classes/ORM models types/DDLs to one standard to allow easy way convert one models to another
in next releases it will be used for converter from one type of models to another.
#. Fixed issue: https://github.com/xnuinside/omymodels/issues/18 "NOW() not recognized as now()"
#. Fixed issue: https://github.com/xnuinside/omymodels/issues/19 "Default value of now() always returns same time, use field for dataclass"

**v0.8.1**


Expand Down
26 changes: 21 additions & 5 deletions omymodels/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@
from jinja2 import Template

from typing import Optional, List, Dict

from typing_extensions import final
from simple_ddl_parser import DDLParser, parse_from_file
from omymodels.gino import core as g
from omymodels.pydantic import core as p
from omymodels.dataclass import core as d
from omymodels.sqlalchemy import core as s
from omymodels.sqlalchemy_core import core as sc
from omymodels.meta_model import TableMeta, Column, Type


def get_tables_information(
Expand Down Expand Up @@ -41,11 +44,10 @@ def create_models(
""" models_type can be: "gino", "dataclass", "pydantic" """
# extract data from ddl file
data = get_tables_information(ddl, ddl_path)
print(data)
if not data['tables'] or data['tables'][0].get("schema", 'NOT EXIST') == 'NOT EXIST':
print("No tables found in DDL. Exit.")
sys.exit(0)
data = remove_quotes_from_strings(data)
data = convert_ddl_to_models(data)
if not data['tables']:
sys.exit(0)
# generate code
output = generate_models_file(
data, singular, naming_exceptions, models_type, schema_global, defaults_off
Expand All @@ -57,6 +59,19 @@ def create_models(
return {"metadata": data, "code": output}


def convert_ddl_to_models(data):
final_data = {'tables': [], 'types': []}
tables = []
for table in data['tables']:
tables.append(TableMeta(**table))
final_data['tables'] = tables
_types = []
for _type in data['types']:
_types.append(Type(**_type))
final_data['types'] = _types
return final_data


def save_models_to_file(models: str, dump_path: str) -> None:
folder = os.path.dirname(dump_path)
if folder:
Expand Down Expand Up @@ -98,7 +113,8 @@ def generate_models_file(
schema_global=schema_global,
defaults_off=defaults_off,
)
header = model_generator.create_header(data["tables"], schema=schema_global)
header = model_generator.create_header(
data["tables"], schema=schema_global)
output = render_jinja2_template(models_type, models_str, header)
return output

Expand Down
110 changes: 58 additions & 52 deletions omymodels/dataclass/core.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from typing import Optional, List, Dict
from omymodels.dataclass import templates as pt
from omymodels.dataclass import templates as dt
from omymodels.utils import create_class_name, enum_number_name_list
from omymodels.dataclass.types import types_mapping, datetime_types

Expand All @@ -13,6 +13,7 @@ def __init__(self):
self.enum_imports = set()
self.custom_types = {}
self.uuid_import = False
self.additional_imports = set()

def add_custom_type(self, _type: str) -> str:
column_type = self.custom_types.get(_type, _type)
Expand All @@ -22,12 +23,12 @@ def add_custom_type(self, _type: str) -> str:
return _type

def generate_attr(self, column: Dict, defaults_off: bool) -> str:
column_str = pt.dataclass_attr
column_str = dt.dataclass_attr

if "." in column["type"]:
_type = column["type"].split(".")[1]
if "." in column.type:
_type = column.type.split(".")[1]
else:
_type = column["type"].lower().split("[")[0]
_type = column.type.lower().split("[")[0]
if self.custom_types:
_type = self.add_custom_type(_type)
if _type == _type:
Expand All @@ -36,31 +37,32 @@ def generate_attr(self, column: Dict, defaults_off: bool) -> str:
self.typing_imports.add(_type.split("[")[0])
elif "datetime" in _type:
self.datetime_import = True
elif "[" in column["type"]:
self.additional_imports.add('field')
elif "[" in column.type:
self.typing_imports.add("List")
_type = f"List[{_type}]"
if _type == "UUID":
self.uuid_import = True
column_str = column_str.format(arg_name=column["name"], type=_type)
if column["default"] and defaults_off is False:
column_str = column_str.format(arg_name=column.name, type=_type)
if column.default and defaults_off is False:
column_str = self.add_column_default(column_str, column)
if (
column["nullable"]
and not (column["default"] and not defaults_off)
column.nullable
and not (column.default and not defaults_off)
and not defaults_off
):
column_str += pt.dataclass_default_attr.format(default=None)
column_str += dt.dataclass_default_attr.format(default=None)
return column_str

@staticmethod
def add_column_default(column_str: str, column: Dict) -> str:
if column["type"].upper() in datetime_types:
if "now" in column["default"]:
if column.type.upper() in datetime_types:
if "now" in column.default.lower():
# todo: need to add other popular PostgreSQL & MySQL functions
column["default"] = "datetime.datetime.now()"
elif "'" not in column["default"]:
column["default"] = f"'{column['default']}'"
column_str += pt.dataclass_default_attr.format(default=column["default"])
column.default = dt.field_datetime_now
elif "'" not in column.default:
column.default = f"'{column['default']}'"
column_str += dt.dataclass_default_attr.format(default=column.default)
return column_str

def generate_model(
Expand All @@ -73,79 +75,83 @@ def generate_model(
**kwargs,
) -> str:
model = ""
if table.get("table_name"):
# mean one model one table
model += "\n\n"
model += (
pt.dataclass_class.format(
class_name=create_class_name(
table["table_name"], singular, exceptions
),
table_name=table["table_name"],
)
) + "\n\n"
columns = {"default": [], "non_default": []}
for column in table["columns"]:
column_str = self.generate_attr(column, defaults_off) + "\n"
if "=" in column_str:
columns["default"].append(column_str)
else:
columns["non_default"].append(column_str)
for column in columns["non_default"]:
model += column
for column in columns["default"]:
model += column

# mean one model one table
model += "\n\n"
model += (
dt.dataclass_class.format(
class_name=create_class_name(
table.name, singular, exceptions
),
table_name=table.name,
)
) + "\n\n"
columns = {"default": [], "non_default": []}
for column in table.columns:
column_str = self.generate_attr(column, defaults_off) + "\n"
if "=" in column_str:
columns["default"].append(column_str)
else:
columns["non_default"].append(column_str)
for column in columns["non_default"]:
model += column
for column in columns["default"]:
model += column
return model

def create_header(self, *args, **kwargs) -> str:
header = ""
if self.enum_imports:
header += pt.enum_import.format(enums=",".join(self.enum_imports)) + "\n"
header += dt.enum_import.format(enums=",".join(self.enum_imports)) + "\n"
if self.uuid_import:
header += pt.uuid_import + "\n"
header += dt.uuid_import + "\n"
if self.datetime_import:
header += pt.datetime_import + "\n"
header += dt.datetime_import + "\n"
if self.typing_imports:
_imports = list(self.typing_imports)
_imports.sort()
header += pt.typing_imports.format(typing_types=", ".join(_imports)) + "\n"
header += pt.dataclass_imports
header += dt.typing_imports.format(typing_types=", ".join(_imports)) + "\n"
if self.additional_imports:
self.additional_imports = f', {",".join(self.additional_imports)}'
else:
self.additional_imports = ''
header += dt.dataclass_imports.format(additional_imports=self.additional_imports)
return header

def generate_type(
self, _type: Dict, singular: bool = False, exceptions: Optional[List] = None
) -> str:
""" method to prepare one Model defention - name & tablename & columns """
type_class = ""
if _type["properties"].get("values"):
if _type.properties.get("values"):
# mean this is a Enum
_type["properties"]["values"].sort()
for num, value in enumerate(_type["properties"]["values"]):
_type.properties["values"].sort()
for num, value in enumerate(_type.properties["values"]):
_value = value.replace("'", "")
if not _value.isnumeric():
type_class += (
pt.enum_value.format(name=value.replace("'", ""), value=value)
dt.enum_value.format(name=value.replace("'", ""), value=value)
+ "\n"
)
sub_type = "str, Enum"
self.enum_imports.add("Enum")
else:
type_class += (
pt.enum_value.format(
dt.enum_value.format(
name=enum_number_name_list.get(num), value=_value
)
+ "\n"
)
sub_type = "IntEnum"
self.enum_imports.add("IntEnum")
class_name = create_class_name(_type["type_name"], singular, exceptions)
class_name = create_class_name(_type.name, singular, exceptions)
type_class = (
"\n\n"
+ (
pt.enum_class.format(class_name=class_name, sub_type=sub_type)
dt.enum_class.format(class_name=class_name, sub_type=sub_type)
+ "\n\n"
)
+ type_class
)
self.custom_types[_type["type_name"]] = ("db.Enum", class_name)
self.custom_types[_type.name] = ("db.Enum", class_name)
return type_class
3 changes: 2 additions & 1 deletion omymodels/dataclass/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

base_model = "BaseModel"

dataclass_imports = """from dataclasses import dataclass"""
dataclass_imports = """from dataclasses import dataclass{additional_imports}"""

dataclass_class = """@dataclass
class {class_name}:"""
Expand All @@ -16,3 +16,4 @@ class {class_name}:"""
enum_import = "from enum import {enums}"

uuid_import = "from uuid import UUID"
field_datetime_now = "field(default_factory=datetime.datetime.now)"
Loading

0 comments on commit e8fbb1a

Please sign in to comment.