From 5e557b0c634e69edde0cdc60cbc1c876d7df4a97 Mon Sep 17 00:00:00 2001 From: Ivan Prunier Date: Sat, 17 Feb 2024 14:53:34 +0100 Subject: [PATCH 1/3] Fix small typos in README.md + add .idea/ to .gitignore (#4) * Add .idea/ to .gitignore * Fix small typos in readme --- .gitignore | 4 +++- README.md | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 56c2ad3..f9c5a60 100644 --- a/.gitignore +++ b/.gitignore @@ -165,4 +165,6 @@ database.db *migration.py .python-version .DS_Store -.nicegui/ \ No newline at end of file +.nicegui/ + +.idea/ \ No newline at end of file diff --git a/README.md b/README.md index 7f24553..f567ef3 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Full [Documentation](https://paperz-org.github.io/blitz/) here. # **What is Blitz ?** -Blitz is a tool that build restfull API on the fly based on a simple and easy to maintain configuration file. +Blitz is a tool that builds restfull APIs on the fly based on a simple and easy to maintain configuration file. Here is an example of how simple a Blitz file is: ```yaml @@ -60,7 +60,7 @@ blitz start your-blitz-app *And yeah, that's it.* -Just add some resources in the blitz file, and you have now a fully functional API with models and the corresponding database schema with all the modern feature you can expect from a modern app like: +Just add some resources in the blitz file, and you now have a fully functional API with models and the corresponding database schema, along with all the modern features you can expect from a modern app like: - Automatic Swagger UI for the API - Admin From b829650e58c4b71e641ad913b1393abeb825d912 Mon Sep 17 00:00:00 2001 From: pbrochar <36422268+pbrochar@users.noreply.github.com> Date: Sat, 17 Feb 2024 18:18:30 +0100 Subject: [PATCH 2/3] Create demo option on create command. (#7) * Create demo option on create command. * Update blitz/models/blitz/field.py Co-authored-by: mde-pach <12986913+mde-pach@users.noreply.github.com> * Remove folder and add in gitignore. * Fix MYPY --------- Co-authored-by: mde-pach <12986913+mde-pach@users.noreply.github.com> --- .gitignore | 4 +- blitz/cli/commands/create.py | 228 ++++++++++++++++++++++++--------- blitz/models/base.py | 5 +- blitz/models/blitz/field.py | 63 +++++---- blitz/models/blitz/resource.py | 1 + tests/test_version.py | 3 +- 6 files changed, 217 insertions(+), 87 deletions(-) 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" From d3f3104a26977df2ae50714a6eb4e9b03eba5fc8 Mon Sep 17 00:00:00 2001 From: mde-pach <12986913+mde-pach@users.noreply.github.com> Date: Sat, 17 Feb 2024 18:25:14 +0100 Subject: [PATCH 3/3] 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 <36422268+pbrochar@users.noreply.github.com> --- blitz/app.py | 24 +++++++++++------- blitz/cli/commands/release.py | 48 ++++++++++++++++++++++------------- blitz/cli/commands/start.py | 33 ++++++++++++++++-------- blitz/cli/commands/swagger.py | 38 +++++++++++++++++---------- blitz/cli/errors/__init__.py | 42 ++++++++++++++++++++++++++++++ blitz/db/migrations.py | 2 +- 6 files changed, 134 insertions(+), 53 deletions(-) create mode 100644 blitz/cli/errors/__init__.py diff --git a/blitz/app.py b/blitz/app.py index 6abccdd..bd51bc2 100644 --- a/blitz/app.py +++ b/blitz/app.py @@ -1,3 +1,4 @@ +import enum from pathlib import Path from blitz.db.errors import NoChangesDetectedError @@ -12,6 +13,11 @@ from semver import Version from loguru import logger +class ReleaseLevel(enum.Enum): + PATCH = "PATCH" + MINOR = "MINOR" + MAJOR = "MAJOR" + class BlitzApp: def __init__( @@ -33,7 +39,7 @@ def __init__( self.resources: list[BlitzResource] = [] self._is_loaded = False self._base_resource_model: type[BaseResourceModel] = BaseResourceModel - self._available_version: list[Version] = [] + self.versions: list[Version] = [] self._load_versions() @@ -54,12 +60,12 @@ def _load_versions(self) -> None: raise ValueError( f"Blitz app {self.name} has a version dir '{version}' without a blitz file inside." ) - self._available_version.append(version) + self.versions.append(version) - self._available_version = sorted(self._available_version) + self.versions = sorted(self.versions) def get_version(self, version: Version) -> "BlitzApp": - if version not in self._available_version: + if version not in self.versions: raise ValueError(f"Version {version} not found for Blitz app {self.name}") return BlitzApp( name=self.name, @@ -118,10 +124,10 @@ def load(self) -> None: self._is_loaded = True - def release(self, level: str, force: bool = False) -> Version: + def release(self, level: ReleaseLevel, force: bool = False) -> Version: clear_metadata() - latest_version = self._available_version[-1] if self._available_version else None + latest_version = self.versions[-1] if self.versions else None # If there is already a released version if latest_version is not None: @@ -131,11 +137,11 @@ def release(self, level: str, force: bool = False) -> Version: # We bump the version regarding the release level match level: - case "major": + case ReleaseLevel.MAJOR: new_version = latest_version.bump_major() - case "minor": + case ReleaseLevel.MINOR: new_version = latest_version.bump_minor() - case "patch": + case ReleaseLevel.PATCH: new_version = latest_version.bump_patch() if self.file.path is None: diff --git a/blitz/cli/commands/release.py b/blitz/cli/commands/release.py index be1a73f..7f507d4 100644 --- a/blitz/cli/commands/release.py +++ b/blitz/cli/commands/release.py @@ -1,31 +1,43 @@ -from typing import Annotated +from typing import Annotated, Optional from blitz import BlitzCore +from blitz.app import ReleaseLevel import typer from rich import print -import enum -from blitz.db.errors import NoChangesDetectedError - - -class ReleaseLevel(enum.Enum): - major = "major" - minor = "minor" - patch = "patch" +from blitz.db.errors import NoChangesDetectedError as MigrationNoChangesDetectedError +from blitz.cli.errors import BlitzAppNotFoundError, MissingBlitzAppNameError, NoChangesDetectedError def release_blitz( - blitz_app_name: Annotated[str, typer.Argument(..., help="Blitz app to release")], - level: Annotated[ReleaseLevel, typer.Argument(..., help="Release level")], + blitz_app_name: Annotated[Optional[str], typer.Argument(help="Blitz app to release")] = None, force: Annotated[bool, typer.Option(help="Force the release even if no changes are detected")] = False, + patch: Annotated[bool, typer.Option(help="Release level patch")] = False, + minor: Annotated[bool, typer.Option(help="Release level minor")] = False, + major: Annotated[bool, typer.Option(help="Release level major")] = False, ) -> None: + # Can't have both --patch and --minor or --major + if sum([patch, minor, major]) > 1: + raise typer.BadParameter("You can't use more than one release level") + release_level = ReleaseLevel.MAJOR if major else ReleaseLevel.MINOR if minor else ReleaseLevel.PATCH + blitz = BlitzCore() - app = blitz.get_app(blitz_app_name) + if blitz_app_name is None: + if len(blitz.apps) == 1: + blitz_app = blitz.apps[0] + else: + raise MissingBlitzAppNameError() + else: + try: + blitz_app = blitz.get_app(blitz_app_name) + except Exception: + raise BlitzAppNotFoundError(blitz_app_name) + try: - new_version = app.release(level.value, force=force) - except NoChangesDetectedError: - typer.echo("No changes detected since the latest version. Use --force to release anyway.") - raise typer.Exit(code=1) - typer.echo(f"Blitz app {blitz_app_name} released at version {new_version}") - typer.echo("You can now start your versioned blitz app by running:") + new_version = blitz_app.release(release_level, force=force) + except MigrationNoChangesDetectedError: + raise NoChangesDetectedError() + + 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]") diff --git a/blitz/cli/commands/start.py b/blitz/cli/commands/start.py index 80dff42..31eb66a 100644 --- a/blitz/cli/commands/start.py +++ b/blitz/cli/commands/start.py @@ -13,10 +13,11 @@ from blitz.settings import get_settings from rich import print +from blitz.cli.errors import BlitzAppNotFoundError, BlitzAppVersionNotFoundError, MissingBlitzAppNameError def start_blitz( - blitz_app_name: Annotated[str, typer.Argument(..., help="Blitz app name")], + blitz_app_name: Annotated[Optional[str], typer.Argument(help="Blitz app name")] = None, admin: Annotated[bool, typer.Option(help="Don't create admin.")] = True, port: Annotated[int, typer.Option(help="Define the port of the server")] = get_settings().BLITZ_PORT, config_route: Annotated[bool, typer.Option(help="Enable the blitz config route.")] = True, @@ -25,17 +26,24 @@ def start_blitz( ) -> None: blitz = BlitzCore() - try: - blitz_app = blitz.get_app(blitz_app_name) - if version is not None: + if blitz_app_name is None: + if len(blitz.apps) == 1: + blitz_app = blitz.apps[0] + else: + raise MissingBlitzAppNameError() + else: + try: + blitz_app = blitz.get_app(blitz_app_name) + except Exception: + raise BlitzAppNotFoundError(blitz_app_name) + + if version is not None: + try: blitz_app = blitz_app.get_version(Version.parse(version)) - except Exception as exc: - print(f"[red bold]There is no blitz app named {blitz_app_name}[/red bold]") - print("To list the available blitz apps run:") - print("[bold] blitz list[bold]") - print(f"Error: {exc}") - raise typer.Exit() + except Exception: + raise BlitzAppVersionNotFoundError(blitz_app, version) + # https://patorjk.com/software/taag/#p=display&f=ANSI%20Shadow&t=BLITZ%200.1.0 print( """[bold medium_purple1] ██████╗ ██╗ ██╗████████╗███████╗ ██████╗ ██╗ ██████╗ @@ -46,7 +54,8 @@ def start_blitz( ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚══════╝ ╚═════╝ ╚═╝╚═╝╚═╝ ╚═════╝ [/bold medium_purple1]""" ) - time.sleep(1) + time.sleep(0.3) + if hot_reload: # Need to be refacto os.environ["BLITZ_APP"] = str(blitz_app.name) @@ -54,9 +63,11 @@ def start_blitz( os.environ["BLITZ_VERSION"] = str(version) os.environ["BLITZ_ADMIN"] = str(admin).lower() os.environ["BLITZ_CONFIG_ROUTE"] = str(config_route).lower() + if blitz_app.file.path is None: # TODO: handle error raise Exception + server_config = uvicorn.Config( "blitz.api:create_blitz_api", factory=True, diff --git a/blitz/cli/commands/swagger.py b/blitz/cli/commands/swagger.py index bc69502..d9fada3 100644 --- a/blitz/cli/commands/swagger.py +++ b/blitz/cli/commands/swagger.py @@ -6,6 +6,7 @@ from blitz.models import BlitzResource from rich.style import Style from rich.panel import Panel +from blitz.cli.errors import BlitzAppNotFoundError, MissingBlitzAppNameError, BlitzAppVersionNotFoundError class SwaggerPrinter: @@ -72,26 +73,35 @@ def print(self) -> None: def list_routes( - blitz_app_name: Annotated[str, typer.Argument(..., help="Blitz app name")], - model: Annotated[Optional[str], typer.Option()] = None, + blitz_app_name: Annotated[Optional[str], typer.Argument(..., help="Blitz app name")] = None, + resource_name: Annotated[Optional[str], typer.Option(help="The resource name")] = None, version: Annotated[Optional[str], typer.Option(help="Define the version of the app.")] = None, ) -> None: blitz = BlitzCore() - try: - blitz_app = blitz.get_app(blitz_app_name) - if version is not None: + if blitz_app_name is None: + if len(blitz.apps) == 1: + blitz_app = blitz.apps[0] + else: + raise MissingBlitzAppNameError() + else: + try: + blitz_app = blitz.get_app(blitz_app_name) + except Exception: + raise BlitzAppNotFoundError(blitz_app_name) + + if version is not None: + try: blitz_app = blitz_app.get_version(Version.parse(version)) - blitz_app.load() - except Exception as exc: - print(f"[red bold]There is no blitz app named {blitz_app_name}[/red bold]") - print("To list the available blitz apps run:") - print("[bold] blitz list[bold]") - print(f"Error: {exc}") - raise typer.Exit() - if model: + except Exception: + raise BlitzAppVersionNotFoundError(blitz_app=blitz_app, version=version) + + blitz_app.load() + + if resource_name: for resource in blitz_app.resources: - if resource.config.name.lower() == model.lower(): + if resource.config.name == resource_name: resources = [resource] else: resources = blitz_app.resources + SwaggerPrinter(resources).print() diff --git a/blitz/cli/errors/__init__.py b/blitz/cli/errors/__init__.py new file mode 100644 index 0000000..39e4f35 --- /dev/null +++ b/blitz/cli/errors/__init__.py @@ -0,0 +1,42 @@ +import typer +from rich import print + +from blitz.app import BlitzApp + + +class BlitzCLIError(typer.Exit): + CODE = 1 + + +class BlitzAppNotFoundError(BlitzCLIError): + def __init__(self, blitz_app_name: str) -> None: + self.blitz_app_name = blitz_app_name + print(f"[red bold]There is no blitz app named {blitz_app_name}[/red bold]") + print("To list the available blitz apps run:") + print("[bold] blitz list[bold]") + super().__init__(code=self.CODE) + + +class BlitzAppVersionNotFoundError(BlitzCLIError): + def __init__(self, blitz_app: BlitzApp, version: str) -> None: + self.blitz_app = blitz_app + self.version = version + print(f"[red bold]There is no version {version} for the blitz app {blitz_app.name}[/red bold]") + print("Available versions are:") + for blitz_app_version in blitz_app.versions: + print(f" {blitz_app_version}") + super().__init__(code=self.CODE) + + +class MissingBlitzAppNameError(BlitzCLIError): + def __init__(self) -> None: + print("You need to specify a blitz app name.") + print("To list the available blitz apps run:") + print("[bold] blitz list[bold]") + super().__init__(code=self.CODE) + + +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) \ No newline at end of file diff --git a/blitz/db/migrations.py b/blitz/db/migrations.py index 0f2c888..e72b7e0 100644 --- a/blitz/db/migrations.py +++ b/blitz/db/migrations.py @@ -42,7 +42,7 @@ def get_alembic_config( if is_release: file_name_template = RELEASE_MIGRATION_FILE_NAME_TEMPLATE migrations_paths = set( - [str(blitz_app.path / str(version)) for version in blitz_app._available_version] + [str(blitz_app.path / str(version)) for version in blitz_app.versions] + [str(blitz_app.path / str(blitz_app.file.config.version))] ) else: