diff --git a/.gitignore b/.gitignore index f9c5a60..99d5061 100644 --- a/.gitignore +++ b/.gitignore @@ -166,5 +166,5 @@ database.db .python-version .DS_Store .nicegui/ - -.idea/ \ No newline at end of file +demo-blitz-app/ +.idea/ diff --git a/blitz/cli/commands/create.py b/blitz/cli/commands/create.py index e8a1275..853effc 100644 --- a/blitz/cli/commands/create.py +++ b/blitz/cli/commands/create.py @@ -6,75 +6,185 @@ from blitz.models.blitz.config import BlitzAppConfig import typer -DEFAULT_VERSION = "0.1.0" +from blitz.models.blitz.resource import BlitzResourceConfig -def write_blitz_file(blitz_file: BlitzFile, blitz_file_format: str) -> Path: - if blitz_file_format == "json": - blitz_file_data = blitz_file.model_dump_json(indent=4, by_alias=True) - elif blitz_file_format == "yaml": - blitz_file_data = yaml.dump(blitz_file.model_dump(by_alias=True), default_flow_style=False) - else: - raise ValueError("Invalid blitz file format") +def get_blitz_demo_resources() -> list[BlitzResourceConfig]: + return [ + BlitzResourceConfig( + name="Food", + fields={ + "name!": "str!", + "expiration_date": "datetime!", + }, + ), + BlitzResourceConfig( + name="Ingredient", + fields={ + "food_id": "Food.id", + "food": "Food", + "recipe_id": "Recipe.id!", + "recipe": "Recipe", + }, + ), + BlitzResourceConfig( + name="Recipe", + fields={ + "name!": "str!", + "ingredients": "Ingredient[]", + "cook_id": "Cook.id!", + "cook": "Cook", + }, + ), + BlitzResourceConfig( + name="Cook", + fields={ + "name!": "str!", + "age": "int!", + "recipes": "Recipe[]", + "rat": "Rat", + }, + ), + BlitzResourceConfig( + name="Rat", + fields={ + "name!": "str!", + "age": "int!", + "cook_id!": "Cook.id!", + "cook": "Cook", + }, + ), + ] + + +class BlitzProjectCreator: + DEFAULT_VERSION = "0.1.0" + DEFAULT_BLITZ_APP_NAME = "Random Blitz App" + DEFAULT_BLIZ_APP_DESCRIPTION = "" + DEFAULT_BLITZ_FILE_FORMAT = "json" + 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: + self.name = name + self.description = description + self.file_format = file_format + self.path = Path(self.name.lower().replace(" ", "-")) + self.demo = demo + + self.blitz_file: BlitzFile | None = None + + def create_directory_or_exit(self) -> None: + if not self.blitz_file: + self.create_file_or_exit() + try: + # Create the blitz app directory, the .blitz file and the blitz file + self._create_directory() + except Exception as e: + self._print_directory_error(e) + raise typer.Exit(code=1) + + def create_file_or_exit(self) -> None: + try: + # Create the blitz file + self._create_file() + except Exception as e: + self._print_file_error(e) + 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("To start your app, you can use:") + print(f" [bold medium_purple1]blitz start {self.path}[/bold medium_purple1]") + + def _create_directory(self) -> None: + self.path.mkdir(parents=True) + blitz_app_file_path = self.path / ".blitz" + blitz_app_file_path.touch() + blitz_file_path = self._write_blitz_file() + with open(blitz_app_file_path, "w") as blitz_app_file: + blitz_app_file.write(str(blitz_file_path)) + + def _create_file(self) -> None: + self.blitz_file = BlitzFile( + path=self.path / f"blitz.{self.file_format}", + config=BlitzAppConfig( + name=self.name, + description=self.description, + version=self.DEFAULT_VERSION, + ), + resources_configs=get_blitz_demo_resources() if self.demo else [], + raw_file={}, + ) - if blitz_file.path is None: - # TODO: handle error - raise Exception - with open(blitz_file.path, "w") as file: - file.write(blitz_file_data) + def _write_blitz_file(self) -> Path: + if self.blitz_file is None: + # TODO Handle error + 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 + ) + case "yaml": + blitz_file_data = yaml.dump( + self.blitz_file.model_dump(by_alias=True, exclude_unset=True), + default_flow_style=False, + ) + case _: + raise ValueError("Invalid blitz file format") - return blitz_file.path + if self.blitz_file.path is None: + # TODO: handle error + raise Exception + with open(self.blitz_file.path, "w") as file: + file.write(blitz_file_data) + return self.blitz_file.path + + @staticmethod + def _print_file_error(error: Exception) -> None: + print(f"[red bold]Error[/red bold] while creating the blitz file: {error}") + + @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}" + ) def create_blitz_app( blitz_app_name: Annotated[ - Optional[str], typer.Argument(help="The name of the blitz app you want to create") + Optional[str], + typer.Argument(help="The name of the blitz app you want to create"), ] = None, + demo: Annotated[bool, typer.Option(help="Create a demo blitz app")] = False, ) -> None: - if not blitz_app_name: - # Interactive prompt to create a new blitz app - blitz_app_name = prompt.Prompt.ask( - "Enter the name of your blitz app", - default="Random Blitz App", + if demo: + name = BlitzProjectCreator.DEMO_BLITZ_APP_NAME + description = BlitzProjectCreator.DEMO_BLITZ_APP_DESCRIPTION + file_format = BlitzProjectCreator.DEFAULT_BLITZ_FILE_FORMAT + else: + if not blitz_app_name: + # Interactive prompt to create a new blitz app + name = prompt.Prompt.ask( + "Enter the name of your blitz app", + default=BlitzProjectCreator.DEFAULT_BLITZ_APP_NAME, + ) + description = prompt.Prompt.ask( + "Enter the description of your blitz app", + default=BlitzProjectCreator.DEFAULT_BLIZ_APP_DESCRIPTION, ) - blitz_app_description = prompt.Prompt.ask( - "Enter the description of your blitz app", - default="", - ) - blitz_file_format = prompt.Prompt.ask( - "Choose the format of the blitz file (can be changed later)", - choices=["json", "yaml"], - default="yaml", - ) - - blitz_app_path = Path(blitz_app_name.lower().replace(" ", "-")) - try: - # Create the blitz file - blitz_file = BlitzFile( - 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_format = prompt.Prompt.ask( + "Choose the format of the blitz file (can be changed later)", + choices=["json", "yaml"], + default=BlitzProjectCreator.DEFAULT_BLITZ_FILE_FORMAT, ) - except Exception as e: - print(f"[red bold]Error[/red bold] while creating the blitz file: {e}") - typer.Exit(code=1) - try: - # Create the blitz app directory, the .blitz file and the blitz file - blitz_app_path.mkdir(parents=True) - blitz_app_file_path = blitz_app_path / ".blitz" - blitz_app_file_path.touch() - blitz_file_path = write_blitz_file(blitz_file, blitz_file_format) - with open(blitz_app_file_path, "w") as blitz_app_file: - blitz_app_file.write(str(blitz_file_path)) - except Exception as e: - print(f"[red bold]Error[/red bold] while creating the blitz app in the file system: {e}") - typer.Exit(code=1) - print(f"\n[medium_purple1 bold]{blitz_app_name}[/medium_purple1 bold] created successfully !") - print("To start your app, you can use:") - print(f" [bold medium_purple1]blitz start {blitz_app_path}[/bold medium_purple1]") + blitz_creator = BlitzProjectCreator(name, description, file_format, demo) + blitz_creator.create_file_or_exit() + blitz_creator.create_directory_or_exit() + blitz_creator.print_success_message() diff --git a/blitz/models/base.py b/blitz/models/base.py index 0cb3d47..534b1e5 100644 --- a/blitz/models/base.py +++ b/blitz/models/base.py @@ -188,12 +188,13 @@ def create_resource_model( field_type = eval(field.relationship) except NameError: field_type = f"{field.relationship}" - if field.relationship_list is True: - field_type = list[field_type] # type: ignore else: field_type = already_created_models[field.relationship] + if field.relationship_list is True: + field_type = list[field_type] # type: ignore else: raise ValueError(f"Relationship `{field.relationship}` is missing.") + else: field_info = Field(**extra) field_type = field.type.value diff --git a/blitz/models/blitz/field.py b/blitz/models/blitz/field.py index 908cb47..e7a72ca 100644 --- a/blitz/models/blitz/field.py +++ b/blitz/models/blitz/field.py @@ -2,7 +2,7 @@ from blitz.models.utils import ContainsEnum from typing import Any, ClassVar -from pydantic import BaseModel, Field, computed_field, field_validator, model_serializer +from pydantic import BaseModel, computed_field, field_validator, model_serializer import uuid from datetime import datetime import logging @@ -52,7 +52,9 @@ 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 @@ -99,13 +101,13 @@ class Config: _raw_field_value: str | dict[str, Any] | None = None type: BlitzType - default: Any = Field(_BlitzNullValue(), exclude=True) - foreign_key: str | _BlitzNullValue = Field(_BlitzNullValue(), exclude=True) - relationship: str | _BlitzNullValue = Field(_BlitzNullValue(), exclude=True) - relationship_list: bool | _BlitzNullValue = Field(_BlitzNullValue(), exclude=True) - back_populates: str | _BlitzNullValue = Field(_BlitzNullValue(), exclude=True) - nullable: bool | _BlitzNullValue = Field(_BlitzNullValue(), exclude=True) - unique: bool | _BlitzNullValue = Field(_BlitzNullValue(), exclude=True) + default: Any | _BlitzNullValue = _BlitzNullValue() + foreign_key: str | _BlitzNullValue = _BlitzNullValue() + relationship: str | _BlitzNullValue = _BlitzNullValue() + relationship_list: bool | _BlitzNullValue = _BlitzNullValue() + back_populates: str | _BlitzNullValue = _BlitzNullValue() + nullable: bool | _BlitzNullValue = _BlitzNullValue() + unique: bool | _BlitzNullValue = _BlitzNullValue() @field_validator("type", mode="before") def _string_to_customtype(cls, v: str | BlitzType) -> BlitzType: @@ -124,15 +126,22 @@ 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) @@ -141,21 +150,29 @@ def from_shortcut_version(cls, raw_field_name: str, raw_field_value: str) -> "Bl else: field_type = AllowedBlitzFieldTypes.relationship + params: dict[str, Any] = {} + params["nullable"] = ( + 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: + params["default"] = None + + if field_type == AllowedBlitzFieldTypes.foreign_key: + params["foreign_key"] = field_value + + if field_type == AllowedBlitzFieldTypes.relationship: + params["relationship"] = field_value + params["relationship_list"] = ( + cls._relationship_list_modifier in field_value_modifiers + ) + return cls( _raw_field_name=raw_field_name, _raw_field_value=raw_field_value, type=field_type, - nullable=cls._nullable_modifier in field_value_modifiers - or field_type == AllowedBlitzFieldTypes.foreign_key, - unique=cls._unique_modifier in field_name_modifiers, - default=None if cls._nullable_modifier in field_value_modifiers else _BlitzNullValue(), - foreign_key=field_value if field_type == AllowedBlitzFieldTypes.foreign_key else _BlitzNullValue(), - relationship=field_value if field_type == AllowedBlitzFieldTypes.relationship else _BlitzNullValue(), - relationship_list=( - cls._relationship_list_modifier in field_value_modifiers - if field_type == AllowedBlitzFieldTypes.relationship - else _BlitzNullValue() - ), + **params, ) def model_shortcut_dump(self) -> str: diff --git a/blitz/models/blitz/resource.py b/blitz/models/blitz/resource.py index ba094f6..f75e9d6 100644 --- a/blitz/models/blitz/resource.py +++ b/blitz/models/blitz/resource.py @@ -23,6 +23,7 @@ def _string_to_fields(cls, v: dict[str, Any | dict[str, Any]]) -> dict[str, Blit # If the field values is a string, it can be an blitz type or a relationship related field if isinstance(raw_field_value, str): 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): fields[field_name] = BlitzField( diff --git a/tests/test_version.py b/tests/test_version.py index f4567b1..f058691 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -1,4 +1,5 @@ from blitz import __version__ + def test_version() -> None: - assert __version__ == "0.1.0" \ No newline at end of file + assert __version__ == "0.1.0"