Skip to content

Commit

Permalink
Change the file format of the blitz file and add internal (need to be…
Browse files Browse the repository at this point in the history
… renamed) fields with their `_` preffix
  • Loading branch information
mde-pach committed Feb 17, 2024
1 parent 81fac60 commit 7e03a73
Show file tree
Hide file tree
Showing 11 changed files with 211 additions and 94 deletions.
8 changes: 4 additions & 4 deletions blitz/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from blitz.models.blitz.field import _BlitzNullValue, AllowedBlitzFieldTypes, BlitzField, BlitzType
from blitz.models.blitz.file import BlitzFile
from blitz.models.blitz.resource import BlitzResource, BlitzResourceConfig
from blitz.parser import _find_blitz_file_path, parse_file
from blitz.parser import find_blitz_file_path
from blitz.db.migrations import generate_migration, run_migrations
import warnings
from sqlalchemy import exc as sa_exc
Expand Down Expand Up @@ -49,7 +49,7 @@ def _load_versions(self) -> None:
continue

try:
_find_blitz_file_path(self.path / str(version))
find_blitz_file_path(self.path / str(version))
except Exception:
raise ValueError(
f"Blitz app {self.name} has a version dir '{version}' without a blitz file inside."
Expand All @@ -64,7 +64,7 @@ def get_version(self, version: Version) -> "BlitzApp":
return BlitzApp(
name=self.name,
path=self.path,
file=parse_file(_find_blitz_file_path(self.path / str(version))),
file=BlitzFile.from_file(find_blitz_file_path(self.path / str(version))),
in_memory=self._in_memory,
version=version,
)
Expand Down Expand Up @@ -143,7 +143,7 @@ def release(self, level: str, force: bool = False) -> Version:
raise Exception
# We run the migrations to the latest version
latest_blitz_app = BlitzApp(
"", latest_version_path, parse_file(latest_version_path / self.file.path.name), in_memory=True
"", latest_version_path, BlitzFile.from_file(latest_version_path / self.file.path.name), in_memory=True
)

with warnings.catch_warnings():
Expand Down
17 changes: 9 additions & 8 deletions blitz/cli/commands/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,16 @@ def create_blitz_app(
blitz_app_path = Path(blitz_app_name.lower().replace(" ", "-"))
try:
# Create the blitz file
blitz_file = BlitzFile(
blitz_file = BlitzFile.from_dict(
blitz_file={
BlitzFile.CONFIG_FIELD_NAME: BlitzAppConfig(
name=blitz_app_name,
description=blitz_app_description,
version=DEFAULT_VERSION,
).model_dump()
},
path=blitz_app_path / f"blitz.{blitz_file_format}",
config=BlitzAppConfig(
name=blitz_app_name,
description=blitz_app_description,
version=DEFAULT_VERSION,
),
resources_configs=[],
raw_file={},
file_type=BlitzFile.FileType(blitz_file_format),
)
except Exception as e:
print(f"[red bold]Error[/red bold] while creating the blitz file: {e}")
Expand Down
9 changes: 5 additions & 4 deletions blitz/core.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from pathlib import Path

from blitz.app import BlitzApp
from blitz.parser import _find_blitz_app_path, _find_blitz_file_path, parse_file
from blitz.models.blitz.file import BlitzFile
from blitz.parser import find_blitz_app_path, find_blitz_file_path
from blitz.settings import DBTypes, get_settings


Expand All @@ -25,9 +26,9 @@ def _discover_apps(self) -> None:

for dotfile in Path(".").glob(f"**/*{self.BLITZ_DOT_FILE}"):
blitz_app_name = dotfile.parent.name
blitz_app_path = _find_blitz_app_path(blitz_app_name)
blitz_file_path = _find_blitz_file_path(blitz_app_path)
blitz_file = parse_file(blitz_file_path)
blitz_app_path = find_blitz_app_path(blitz_app_name)
blitz_file_path = find_blitz_file_path(blitz_app_path)
blitz_file = BlitzFile.from_file(blitz_file_path)

self.apps.append(
BlitzApp(
Expand Down
6 changes: 6 additions & 0 deletions blitz/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ def create_resource_model(
fields: dict[Any, Any] = {}
for field_name, field in resource_config.fields.items():
extra = {}

if not isinstance(field.default, _BlitzNullValue):
extra["default"] = field.default

Expand All @@ -178,6 +179,10 @@ def create_resource_model(
if not isinstance(field.unique, _BlitzNullValue):
extra["unique"] = field.unique

if field.settings and field.settings.description is not None:
extra["description"] = field.settings.description
extra["title"] = field.settings.description

if field.type == AllowedBlitzFieldTypes.foreign_key:
pass
elif field.type == AllowedBlitzFieldTypes.relationship:
Expand All @@ -196,6 +201,7 @@ def create_resource_model(
raise ValueError(f"Relationship `{field.relationship}` is missing.")
else:
field_info = Field(**extra)
print(field_info)
field_type = field.type.value

if extra.get("nullable", False) is True:
Expand Down
8 changes: 7 additions & 1 deletion blitz/models/blitz/field.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ class BlitzField(BaseModel):
class Config:
arbitrary_types_allowed = True

class Settings(BaseModel):
FIELD_PREFIX: ClassVar[str] = "_"
description: str | None = None

settings: Settings | None = Field(None, exclude=True)

# Modifiers are used to define the properties of a field in the shortcut version of the blitz field
_unique_modifier: ClassVar[str] = "!"
_nullable_modifier: ClassVar[str] = "?"
Expand Down Expand Up @@ -115,7 +121,7 @@ def _string_to_customtype(cls, v: str | BlitzType) -> BlitzType:

# Need a fix in pydantic maybe use a custom method to serialize the model and not the @model_serializer
# @model_serializer
# def _serialize_model(self) -> dict[str, Any] | str:
# def _serialize_model(self) -> dict[str, Any]:
# if isinstance(self._raw_field_value, dict):
# return self.model_dump()
# elif isinstance(self._raw_field_value, str):
Expand Down
83 changes: 75 additions & 8 deletions blitz/models/blitz/file.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,94 @@
from typing import Any
from pydantic import BaseModel, Field
from typing import Any, ClassVar, NoReturn
from pydantic import BaseModel, Field, field_serializer
from blitz.models.blitz.config import BlitzAppConfig
from blitz.models.blitz.resource import BlitzResourceConfig
from pathlib import Path
from enum import StrEnum
import json
import yaml


class FileType(StrEnum):
JSON = "json"
YAML = "yaml"
def _get_data_from_json(file: Path) -> dict[str, dict[str, Any]]:
with open(file, "r") as f:
return dict(json.load(f))


def _get_data_from_yaml(file: Path) -> dict[str, dict[str, Any]]:
with open(file, "r") as f:
return dict(yaml.safe_load(f))


def _no_parser_for_suffix(file: Path) -> NoReturn:
raise ValueError(f"No parser for {file}")


class BlitzFile(BaseModel):
"""
The Blitz file is the configuration file for a Blitz app. It contains the BlitzAppConfig and a list of BlitzResourceConfig.
"""

path: Path | None = Field(default=None, exclude=True)
file_type: FileType | None = Field(default=None, exclude=True)
class FileType(StrEnum):
JSON = "json"
YAML = "yaml"

CONFIG_FIELD_NAME: ClassVar[str] = "config"
RESOURCES_FIELD_NAME: ClassVar[str] = "resources"

config: BlitzAppConfig
resources_configs: list[BlitzResourceConfig] = Field([], serialization_alias="resources")
resources_configs: list[BlitzResourceConfig] = Field(default=[], serialization_alias=RESOURCES_FIELD_NAME)
raw_file: dict[str, Any] = Field(exclude=True)
path: Path | None = Field(default=None, exclude=True)
file_type: FileType | None = Field(default=None, exclude=True)

# def write(self) -> None:
# with open(self.path, "w") as blitz_file:
# blitz_file.write(self.model_dump_json)

@field_serializer("resources_configs")
def _serialize_resources_configs(self, resources_configs: list[BlitzResourceConfig], _info: Any) -> dict[str, Any]:
serialized_resources_configs = {}
for resource_config in resources_configs:
serialized_resources_configs[resource_config.name] = resource_config.model_dump()

return serialized_resources_configs

@classmethod
def from_file(cls, file_path: Path) -> "BlitzFile":
blitz_file = {
cls.FileType.JSON.value: _get_data_from_json,
cls.FileType.YAML.value: _get_data_from_yaml,
}.get(file_path.suffix[1:], _no_parser_for_suffix)(file_path)

return cls.from_dict(
blitz_file,
path=file_path.absolute(),
file_type=cls.FileType(file_path.suffix.removeprefix(".")),
)

@classmethod
def from_dict(
cls,
blitz_file: dict[str, dict[str, Any]],
path: Path | None = None,
file_type: FileType | None = None,
) -> "BlitzFile":
resources_configs: list[BlitzResourceConfig] = []
resource_name: str
resource_config: dict[str, Any]
for resource_name, resource_config in blitz_file.get(cls.RESOURCES_FIELD_NAME, {}).items():
settings_fields = {}
fields = {}
for field_name, field_value in resource_config.items():
if field_name.startswith(BlitzResourceConfig.Settings.FIELD_PREFIX):
settings_fields[field_name[len(BlitzResourceConfig.Settings.FIELD_PREFIX) :]] = field_value
else:
fields[field_name] = field_value
resources_configs.append(BlitzResourceConfig(name=resource_name, fields=fields, settings=settings_fields))

return cls(
config=blitz_file.get(cls.CONFIG_FIELD_NAME),
resources_configs=resources_configs,
raw_file=blitz_file,
path=path,
file_type=file_type,
)
94 changes: 86 additions & 8 deletions blitz/models/blitz/resource.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from typing import Any
from pydantic import BaseModel, field_validator
from typing import Any, ClassVar
from pydantic import BaseModel, field_validator, model_serializer
from blitz.models.blitz.field import BlitzField
from blitz.models.base import BaseResourceModel

Expand All @@ -11,9 +11,14 @@ class BlitzResourceConfig(BaseModel):
If the fields is a string, we are reading it like a shortcut version of a BlitzField object.
"""

class Settings(BaseModel):
FIELD_PREFIX: ClassVar[str] = "_"
allowed_methods: str = "CRUD"
description: str | None = None

name: str
allowed_methods: str = "CRUD"
fields: dict[str, BlitzField]
settings: Settings

@field_validator("fields", mode="before")
def _string_to_fields(cls, v: dict[str, Any | dict[str, Any]]) -> dict[str, BlitzField]:
Expand All @@ -25,30 +30,49 @@ def _string_to_fields(cls, v: dict[str, Any | dict[str, Any]]) -> dict[str, Blit
fields[field_name] = BlitzField.from_shortcut_version(raw_field_name, raw_field_value)
# Else if the field value is a dict, it must be a BlitzField object
elif isinstance(raw_field_value, dict):
settings_fields_config = {}
field_config = {}
for field_config_name, field_config_value in raw_field_value.items():
if field_config_name.startswith(BlitzField.Settings.FIELD_PREFIX):
settings_fields_config[
field_config_name[len(BlitzField.Settings.FIELD_PREFIX) :]
] = field_config_value
else:
field_config[field_config_name] = field_config_value
fields[field_name] = BlitzField(
_raw_field_name=raw_field_name,
_raw_field_value=raw_field_value,
**raw_field_value,
settings=settings_fields_config,
**field_config,
)
else:
raise ValueError(f"Type `{type(raw_field_value)}` not allowed for field `{raw_field_name}`")
return fields

@model_serializer
def _serialize_model(self) -> dict[str, Any]:
serialized_model = {}
for field_name, field in self.fields.items():
serialized_model[field_name] = field.model_dump()
for setting_name, setting_value in self.settings.model_dump(exclude_unset=True).items():
serialized_model[f"{self.Settings.FIELD_PREFIX}{setting_name}"] = setting_value
return serialized_model

@property
def can_create(self) -> bool:
return "C" in self.allowed_methods
return "C" in self.settings.allowed_methods

@property
def can_read(self) -> bool:
return "R" in self.allowed_methods
return "R" in self.settings.allowed_methods

@property
def can_update(self) -> bool:
return "U" in self.allowed_methods
return "U" in self.settings.allowed_methods

@property
def can_delete(self) -> bool:
return "D" in self.allowed_methods
return "D" in self.settings.allowed_methods


class BlitzResource(BaseModel):
Expand All @@ -59,3 +83,57 @@ class BlitzResource(BaseModel):

config: BlitzResourceConfig
model: type[BaseResourceModel]


BlitzResourceConfig(
name="Food",
fields={
"name!": "str!",
"expiration_date": "datetime!",
},
settings={},
)

BlitzResourceConfig(
name="Ingredient",
fields={
"food_id": "Food.id",
"food": "Food",
"recipe_id": "Recipe.id!",
"recipe": "Recipe",
},
settings={},
)

BlitzResourceConfig(
name="Recipe",
fields={
"name!": "str!",
"ingredients": "Ingredient[]",
"cook_id": "Cook.id!",
"cook": "Cook",
},
settings={},
)

BlitzResourceConfig(
name="Cook",
fields={
"name!": "str!",
"age": "int!",
"recipes": "Recipe[]",
"rat": "Rat",
},
settings={},
)

BlitzResourceConfig(
name="Rat",
fields={
"name!": "str!",
"age": "int!",
"cook_id!": "Cook.id!",
"cook": "Cook",
},
settings={},
)
Loading

0 comments on commit 7e03a73

Please sign in to comment.