Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ jobs:
DB_DATABASE: database_app_test
DB_USERNAME: app
DB_PASSWORD: secret
POSTGRES_PORT: 15432

steps:
- uses: actions/checkout@v4
Expand All @@ -33,7 +34,7 @@ jobs:
# ── fastapi_startkit package ──────────────────────────────────────────
- name: Install dependencies (fastapi_startkit)
working-directory: fastapi_startkit
run: uv sync --group dev --extra database --extra sqlite --extra fastapi --extra vite
run: uv sync --group dev --extra database --extra sqlite --extra fastapi --extra vite --extra postgres

- name: Run tests (fastapi_startkit)
working-directory: fastapi_startkit
Expand Down
8 changes: 8 additions & 0 deletions bin/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ set -e

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"

# ── Lint ──────────────────────────────────────────────────────────────────────
echo "============================================================"
echo " Running: ruff lint checks"
echo "============================================================"
(cd "$ROOT/fastapi_startkit" && uv run ruff format --check src/ tests/)
(cd "$ROOT/fastapi_startkit" && uv run ruff check src/ tests/)

# ── Start MySQL via Docker Compose ────────────────────────────────────────────
echo "Starting test services..."
docker compose -f "$ROOT/docker-compose.yml" down --remove-orphans
Expand All @@ -14,6 +21,7 @@ trap 'echo "Stopping test services..."; docker compose -f "$ROOT/docker-compose.
# ── Common DB env vars (match docker-compose.test.yml) ───────────────────────
export DB_HOST=127.0.0.1
export DB_PORT=3306
export POSTGRES_PORT=15432
export DB_DATABASE=database_app_test
export DB_USERNAME=app
export DB_PASSWORD=secret
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ services:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
ports:
- "5432:5432"
- "15432:5432"
healthcheck:
test: [ "CMD", "pg_isready", "-U", "app", "-d", "database_app_test" ]
interval: 5s
Expand Down
2 changes: 1 addition & 1 deletion example/config-app/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion example/database-app/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions fastapi_startkit/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ dev = [
"ruff>=0.9.0",
"twine>=6.2.0",
"itsdangerous>=2.2.0",
"asyncpg>=0.29.0",
"aiosqlite>=0.22.1",
"aiomysql>=0.2.0",
"sqlalchemy[asyncio]>=2.0.38",
"fastapi[standard]>=0.124.4",
]


Expand Down
12 changes: 3 additions & 9 deletions fastapi_startkit/src/fastapi_startkit/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,7 @@ def load_environment(self):
return self

def configure_exception_handler(self):
self.exception_manager: ExceptionHandler = self._exception_handler_class(
application=self
)
self.exception_manager: ExceptionHandler = self._exception_handler_class(application=self)
self.exception_manager.register()
self.exception_manager.install()
self.bind("exception_manager", self.exception_manager)
Expand Down Expand Up @@ -163,9 +161,7 @@ def mount(self, path: str, app_instance: "FastAPI", **kwargs):
return self

# Add custom exception handlers
def add_exception_handler(
self, exc_class_or_status_code: Any, handler: Callable[..., Any]
):
def add_exception_handler(self, exc_class_or_status_code: Any, handler: Callable[..., Any]):
self._fastapi.add_exception_handler(exc_class_or_status_code, handler)
return self

Expand All @@ -175,9 +171,7 @@ def fastapi(self) -> "FastAPI":
try:
from fastapi import FastAPI
except ImportError:
raise RuntimeError(
"FastAPI is not installed. Install it with: pip install fastapi"
)
raise RuntimeError("FastAPI is not installed. Install it with: pip install fastapi")
self._fastapi = FastAPI()
# Making the type hint work
assert self._fastapi is not None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,9 +276,7 @@ def pluck(self, value, key=None, keep_nulls=True):

if k == value:
if key:
attributes[self._data_get(item, key)] = self._data_get(
item, value
)
attributes[self._data_get(item, key)] = self._data_get(item, value)
else:
attributes.append(v)

Expand Down
4 changes: 1 addition & 3 deletions fastapi_startkit/src/fastapi_startkit/config/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@

@dataclass
class AppConfig:
name: str = field(
default_factory=lambda: os.getenv("APP_NAME", "FastAPI starter kit")
)
name: str = field(default_factory=lambda: os.getenv("APP_NAME", "FastAPI starter kit"))
env: str = field(default_factory=lambda: os.getenv("APP_ENV", "development"))
debug: bool = field(default_factory=lambda: env("APP_DEBUG", "true"))
timezone: str = field(default_factory=lambda: os.getenv("APP_TIMEZONE", "UTC"))
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@ def __init__(self, application):
def load(self):
"""At boot load configuration from all files and store them in here."""
config_root = self.application.make("config.location")
for module_name, module in (
Loader().get_modules(config_root, raise_exception=True).items()
):
for module_name, module in Loader().get_modules(config_root, raise_exception=True).items():
params = Loader().get_parameters(module)
for name, value in params.items():
self._config[f"{module_name}.{name.lower()}"] = value
Expand All @@ -42,9 +40,7 @@ def merge_with(self, path, external_config):
(such as 'application').
"""
if path in self.reserved_keys:
raise InvalidConfigurationSetup(
f"{path} is a reserved configuration key name. Please use an other key."
)
raise InvalidConfigurationSetup(f"{path} is a reserved configuration key name. Please use an other key.")
if isinstance(external_config, str):
# config is a path and should be loaded
params = Loader().get_parameters(external_config)
Expand Down
5 changes: 1 addition & 4 deletions fastapi_startkit/src/fastapi_startkit/console/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
from .application import ConsoleApplication
from .command import Command

__all__ = [
"ConsoleApplication",
"Command"
]
__all__ = ["ConsoleApplication", "Command"]
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,7 @@ def handle(self):
if provider_arg:
target = Str.slugify(provider_arg)
resources = {
name: files
for name, files in application.published_resources.items()
if Str.slugify(name) == target
name: files for name, files in application.published_resources.items() if Str.slugify(name) == target
}
if not resources:
self.line(f"<error>No provider found matching '{provider_arg}'.</error>")
Expand Down
57 changes: 15 additions & 42 deletions fastapi_startkit/src/fastapi_startkit/container/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,10 @@ def bind(self, name, class_obj):
"""
if inspect.ismodule(class_obj):
raise StrictContainerException(
"Cannot bind module '{}' with key '{}' into the container".format(
class_obj, name
)
"Cannot bind module '{}' with key '{}' into the container".format(class_obj, name)
)
if self.strict and name in self.objects:
raise StrictContainerException(
"You cannot override a key inside a strict container"
)
raise StrictContainerException("You cannot override a key inside a strict container")

if self.override or name not in self.objects:
self.fire_hook("bind", name, class_obj)
Expand Down Expand Up @@ -145,9 +141,7 @@ def make(self, name, *arguments):
obj = self.resolve(name, *arguments)
return obj

raise MissingContainerBindingNotFound(
"{0} key was not found in the container".format(name)
)
raise MissingContainerBindingNotFound("{0} key was not found in the container".format(name))

def has(self, name):
"""Check if a key exists in the container.
Expand Down Expand Up @@ -200,14 +194,9 @@ def resolve(self, obj, *resolving_arguments):
self.remember
and not passing_arguments
and inspect.ismethod(obj)
and "{}.{}.{}".format(
obj.__module__, obj.__self__.__class__.__name__, obj.__name__
)
in self._remembered
and "{}.{}.{}".format(obj.__module__, obj.__self__.__class__.__name__, obj.__name__) in self._remembered
):
location = "{}.{}.{}".format(
obj.__module__, obj.__self__.__class__.__name__, obj.__name__
)
location = "{}.{}.{}".format(obj.__module__, obj.__self__.__class__.__name__, obj.__name__)
objects = self._remembered[location]
try:
return obj(*objects)
Expand Down Expand Up @@ -271,9 +260,7 @@ def resolve(self, obj, *resolving_arguments):
if not inspect.ismethod(obj):
self._remembered[obj] = objects
else:
signature = "{}.{}.{}".format(
obj.__module__, obj.__self__.__class__.__name__, obj.__name__
)
signature = "{}.{}.{}".format(obj.__module__, obj.__self__.__class__.__name__, obj.__name__)
self._remembered[signature] = objects
return obj(*objects)

Expand Down Expand Up @@ -305,20 +292,15 @@ def collect(self, search):
providers.update({key: value})
elif "*" in search:
split_search = search.split("*")
if key.startswith(split_search[0]) and key.endswith(
split_search[1]
):
if key.startswith(split_search[0]) and key.endswith(split_search[1]):
providers.update({key: value})
else:
raise AttributeError(
"There is no '*' in your collection search"
)
raise AttributeError("There is no '*' in your collection search")
else:
for provider_key, provider_class in self.objects.items():
if (
inspect.isclass(provider_class)
and issubclass(provider_class, search)
) or isinstance(provider_class, search):
if (inspect.isclass(provider_class) and issubclass(provider_class, search)) or isinstance(
provider_class, search
):
providers.update({provider_key: provider_class})

return providers
Expand All @@ -344,10 +326,7 @@ def _find_annotated_parameter(self, parameter):
return obj

for _, provider_class in self.objects.items():
if (
parameter.annotation == provider_class
or parameter.annotation == provider_class.__class__
):
if parameter.annotation == provider_class or parameter.annotation == provider_class.__class__:
obj = provider_class
self.fire_hook("resolve", parameter, obj)

Expand All @@ -362,9 +341,7 @@ def _find_annotated_parameter(self, parameter):
return obj

raise ContainerError(
"The dependency with the {0} annotation could not be resolved by the container".format(
parameter
)
"The dependency with the {0} annotation could not be resolved by the container".format(parameter)
)

def get_parameters(self, obj):
Expand Down Expand Up @@ -392,9 +369,7 @@ def _find_parameter(self, keyword):
return keyword.default

raise ContainerError(
"The parameter dependency with the key of {0} could not be found in the container".format(
parameter
)
"The parameter dependency with the key of {0} could not be found in the container".format(parameter)
)

def on_bind(self, key, obj):
Expand Down Expand Up @@ -503,9 +478,7 @@ def _find_obj(self, obj):
return return_obj

raise MissingContainerBindingNotFound(
"The dependency with the {0} annotation could not be resolved by the container".format(
obj
)
"The dependency with the {0} annotation could not be resolved by the container".format(obj)
)

def __contains__(self, obj):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,7 @@ def handle(self, exception):
response = self.application.make("response")
request = self.application.make("request")

self.application.make("event").fire(
f"masonite.exception.{exception.__class__.__name__}", exception
)
self.application.make("event").fire(f"masonite.exception.{exception.__class__.__name__}", exception)

# add headers to response if any
if hasattr(exception, "get_headers"):
Expand All @@ -47,17 +45,13 @@ def handle(self, exception):
response.with_headers(headers)

if self.application.has(f"{exception.__class__.__name__}Handler"):
return self.application.make(
f"{exception.__class__.__name__}Handler"
).handle(exception)
return self.application.make(f"{exception.__class__.__name__}Handler").handle(exception)

# handle exception in production
if not self.application.is_debug():
# for HTTP error codes (500, 404, 403...) a specific page should be displayed
# if a renderable exception is raised let it be displayed
if hasattr(exception, "is_http_exception") or hasattr(
exception, "get_response"
):
if hasattr(exception, "is_http_exception") or hasattr(exception, "get_response"):
return self.application.make("HttpExceptionHandler").handle(exception)

# else fallback to an unknown exception that should be displayed as a 500 error
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,7 @@ class DumpExceptionHandler:
def __init__(self, application):
self.application = application

self.assets_path = os.path.join(
get_module_dir(__file__), "../../templates/assets"
)
self.assets_path = os.path.join(get_module_dir(__file__), "../../templates/assets")
self.styles = []
self.scripts = []

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@ def handle(self, exception):
# Renders HTTP exception as HTML with predefined error page if exists
if self.application.make("view").exists(view_name):
return response.view(
self.application.make("view").render(
f"errors/{status_code}", {"message": exception.get_response()}
),
self.application.make("view").render(f"errors/{status_code}", {"message": exception.get_response()}),
status_code,
)
else:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ def __init__(self, application):
self.application = application

def handle(self, exception):
masonite_exception = ModelNotFoundException(
"No record found with the given primary key"
)
masonite_exception = ModelNotFoundException("No record found with the given primary key")
self.application.make("response").status(404)
self.application.make("exception_handler").handle(masonite_exception)
7 changes: 1 addition & 6 deletions fastapi_startkit/src/fastapi_startkit/exceptions/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from typing import Any, Callable, Dict, List, Optional, Type



class ExceptionHandler:
def __init__(self, application=None):
self.app = application
Expand Down Expand Up @@ -74,11 +73,7 @@ def _build_context(self, exception: Exception) -> str:

context = f"{type(exception).__name__}: {exception}"
if self.app and self.app.is_debug():
context += "\n" + "".join(
traceback.format_exception(
type(exception), exception, exception.__traceback__
)
)
context += "\n" + "".join(traceback.format_exception(type(exception), exception, exception.__traceback__))
return context

async def handle(self, exception: Exception, context: Optional[Dict] = None) -> Any:
Expand Down
4 changes: 1 addition & 3 deletions fastapi_startkit/src/fastapi_startkit/facades/Hash.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@ class Hash:
) -> bool:
"""Verify that a given string matches its hashed version (based on configured hashing protocol)."""
...
def needs_rehash(
hashed_string: str, options: dict = {}, driver: str = None
) -> bool:
def needs_rehash(hashed_string: str, options: dict = {}, driver: str = None) -> bool:
"""Verify that a given hash needs to be hashed again because parameters for generating
the hash have changed."""
...
Loading
Loading