Skip to content

Commit

Permalink
Merge branch 'main' into feature/blitz-app-name-not-required-if-alone
Browse files Browse the repository at this point in the history
  • Loading branch information
mde-pach committed Feb 17, 2024
2 parents 42e9fb4 + b829650 commit 87bf73b
Show file tree
Hide file tree
Showing 6 changed files with 217 additions and 87 deletions.
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -166,5 +166,5 @@ database.db
.python-version
.DS_Store
.nicegui/

.idea/
demo-blitz-app/
.idea/
228 changes: 169 additions & 59 deletions blitz/cli/commands/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
5 changes: 3 additions & 2 deletions blitz/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
63 changes: 40 additions & 23 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, 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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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)
Expand All @@ -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:
Expand Down
1 change: 1 addition & 0 deletions blitz/models/blitz/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
3 changes: 2 additions & 1 deletion tests/test_version.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from blitz import __version__


def test_version() -> None:
assert __version__ == "0.1.0"
assert __version__ == "0.1.0"

0 comments on commit 87bf73b

Please sign in to comment.