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 (#6)

* Change the file format of the blitz file and add internal (need to be renamed) fields with their `_` preffix

* Update blitz/models/base.py

* Fix small typos in README.md + add .idea/ to .gitignore (#4)

* Add .idea/ to .gitignore

* Fix small typos in readme

* Create demo option on create command. (#7)

* Create demo option on create command.

* Update blitz/models/blitz/field.py

Co-authored-by: mde-pach <[email protected]>

* Remove folder and add in gitignore.

* Fix MYPY

---------

Co-authored-by: mde-pach <[email protected]>

* Add exception that handle error message printing in the cli (temporary fatorization) and use the only existing blitz app if there is only one and no blitz app name is specified (#5)

* Add exception that handle error message printing in the cli (temporary fatorization) and use the only existing blitz app if there is only one and no blitz app name is specified

* Update blitz/cli/commands/start.py

---------

Co-authored-by: pbrochar <[email protected]>

* reformat code

---------

Co-authored-by: pbrochar <[email protected]>
Co-authored-by: Ivan Prunier <[email protected]>
  • Loading branch information
3 people authored Feb 17, 2024
1 parent d3f3104 commit c8161f9
Show file tree
Hide file tree
Showing 13 changed files with 216 additions and 120 deletions.
9 changes: 5 additions & 4 deletions blitz/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
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
from semver import Version
from loguru import logger


class ReleaseLevel(enum.Enum):
PATCH = "PATCH"
MINOR = "MINOR"
Expand Down Expand Up @@ -55,7 +56,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 @@ -70,7 +71,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 @@ -149,7 +150,7 @@ def release(self, level: ReleaseLevel, 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
16 changes: 4 additions & 12 deletions blitz/cli/commands/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,7 @@ class BlitzProjectCreator:
DEMO_BLITZ_APP_DESCRIPTION = "This is a demo blitz app"
DEMO_BLITZ_APP_NAME = "Demo Blitz App"

def __init__(
self, name: str, description: str, file_format: str, demo: bool = False
) -> None:
def __init__(self, name: str, description: str, file_format: str, demo: bool = False) -> None:
self.name = name
self.description = description
self.file_format = file_format
Expand Down Expand Up @@ -95,9 +93,7 @@ def create_file_or_exit(self) -> None:
raise typer.Exit(code=1)

def print_success_message(self) -> None:
print(
f"\n[medium_purple1 bold]{self.name}[/medium_purple1 bold] created successfully !"
)
print(f"\n[medium_purple1 bold]{self.name}[/medium_purple1 bold] created successfully !")
print("To start your app, you can use:")
print(f" [bold medium_purple1]blitz start {self.path}[/bold medium_purple1]")

Expand Down Expand Up @@ -127,9 +123,7 @@ def _write_blitz_file(self) -> Path:
raise Exception()
match self.file_format:
case "json":
blitz_file_data = self.blitz_file.model_dump_json(
indent=4, by_alias=True, exclude_unset=True
)
blitz_file_data = self.blitz_file.model_dump_json(indent=4, by_alias=True, exclude_unset=True)
case "yaml":
blitz_file_data = yaml.dump(
self.blitz_file.model_dump(by_alias=True, exclude_unset=True),
Expand All @@ -151,9 +145,7 @@ def _print_file_error(error: Exception) -> None:

@staticmethod
def _print_directory_error(error: Exception) -> None:
print(
f"[red bold]Error[/red bold] while creating the blitz app in the file system: {error}"
)
print(f"[red bold]Error[/red bold] while creating the blitz app in the file system: {error}")


def create_blitz_app(
Expand Down
4 changes: 2 additions & 2 deletions blitz/cli/commands/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,6 @@ def release_blitz(
except MigrationNoChangesDetectedError:
raise NoChangesDetectedError()

print(f"Blitz app {blitz_app_name} released at version {new_version}")
print(f"Blitz app {blitz_app.name} released at version {new_version}")
print("You can now start your versioned blitz app by running:")
print(f" [bold medium_purple1]blitz start {blitz_app_name} --version {new_version}[/bold medium_purple1]")
print(f" [bold medium_purple1]blitz start {blitz_app.name} --version {new_version}[/bold medium_purple1]")
2 changes: 1 addition & 1 deletion blitz/cli/errors/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,4 @@ def __init__(self) -> None:
class NoChangesDetectedError(BlitzCLIError):
def __init__(self) -> None:
print("No changes detected since the latest version. Use --force to release anyway.")
super().__init__(code=self.CODE)
super().__init__(code=self.CODE)
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
5 changes: 5 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 Down
34 changes: 14 additions & 20 deletions blitz/models/blitz/field.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from blitz.models.utils import ContainsEnum
from typing import Any, ClassVar

from pydantic import BaseModel, computed_field, field_validator, model_serializer
from pydantic import BaseModel, computed_field, field_validator, model_serializer, Field
import uuid
from datetime import datetime
import logging
Expand Down Expand Up @@ -52,9 +52,7 @@ class BlitzType(BaseModel):
def __init_subclass__(cls, **kwargs: Any) -> None:
for allowed_type in AllowedBlitzFieldTypes:
if allowed_type not in cls.TYPE_MAPPING:
logger.warning(
f"Type {allowed_type} is not mapped with a factory in {cls.__name__}.TYPE_MAPPING."
)
logger.warning(f"Type {allowed_type} is not mapped with a factory in {cls.__name__}.TYPE_MAPPING.")

@computed_field # type: ignore
@property
Expand All @@ -81,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 = Field(Settings(), 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 @@ -117,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 All @@ -126,22 +130,15 @@ def _string_to_customtype(cls, v: str | BlitzType) -> BlitzType:
# raise ValueError(f"Type `{type(self._raw_field_value)}` not allowed")

@classmethod
def from_shortcut_version(
cls, raw_field_name: str, raw_field_value: str
) -> "BlitzField":
def from_shortcut_version(cls, raw_field_name: str, raw_field_value: str) -> "BlitzField":
field_name = raw_field_name.strip(cls._field_name_shortcut_modifiers)
field_name_modifiers = raw_field_name[len(field_name) :]

field_value = raw_field_value.strip(cls._field_value_shortcut_modifiers)
field_value_modifiers = raw_field_value[len(field_value) :]

if (
cls._required_modifier in field_value_modifiers
and cls._nullable_modifier in field_value_modifiers
):
raise ValueError(
f"Field `{field_name}` cannot be both required and nullable."
)
if cls._required_modifier in field_value_modifiers and cls._nullable_modifier in field_value_modifiers:
raise ValueError(f"Field `{field_name}` cannot be both required and nullable.")

if field_value in AllowedBlitzFieldTypes:
field_type = AllowedBlitzFieldTypes(field_value)
Expand All @@ -152,8 +149,7 @@ def from_shortcut_version(

params: dict[str, Any] = {}
params["nullable"] = (
cls._nullable_modifier in field_value_modifiers
or field_type == AllowedBlitzFieldTypes.foreign_key
cls._nullable_modifier in field_value_modifiers or field_type == AllowedBlitzFieldTypes.foreign_key
)
params["unique"] = cls._unique_modifier in field_name_modifiers
if cls._nullable_modifier in field_value_modifiers:
Expand All @@ -164,9 +160,7 @@ def from_shortcut_version(

if field_type == AllowedBlitzFieldTypes.relationship:
params["relationship"] = field_value
params["relationship_list"] = (
cls._relationship_list_modifier in field_value_modifiers
)
params["relationship_list"] = cls._relationship_list_modifier in field_value_modifiers

return cls(
_raw_field_name=raw_field_name,
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,
)
Loading

0 comments on commit c8161f9

Please sign in to comment.