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
14 changes: 1 addition & 13 deletions fastapi_startkit/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "fastapi-startkit"
version = "0.13.7"
version = "0.13.6"
description = "Fastapi Starter kit components"
authors = [
{name = "Bedram Tamang", email = "tmgbedu@gmail.com"}
Expand Down Expand Up @@ -46,7 +46,6 @@ dev = [
"dumpdie>=1.5.0",
"pytest>=9.0.3",
"pytest-asyncio>=1.3.0",
"ruff>=0.15.12",
"twine>=6.2.0",
]

Expand All @@ -60,14 +59,3 @@ build-backend = "uv_build"

[tool.uv]
workspace = { members = ["application", "example/*"] }

[tool.ruff]
line-length = 120

[tool.ruff.lint]
select = ["E", "F", "I"]
ignore = ["E501"]
fixable = ["ALL"]

[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401"]
25 changes: 15 additions & 10 deletions fastapi_startkit/src/fastapi_startkit/application.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import os
from pathlib import Path
from typing import TYPE_CHECKING, Any, Callable, Generic, List, Optional, Type, TypeVar
from typing import TYPE_CHECKING, Optional
from typing import Type, Callable, Any, List, TypeVar, Generic

from fastapi_startkit.providers.app_provider import AppProvider

from .config import AppConfig
from .configuration.providers import ConfigurationProvider
from .container import Container
from .environment.environment import LoadEnvironment

if TYPE_CHECKING:
from fastapi import APIRouter, FastAPI
from fastapi import FastAPI, APIRouter
from starlette.middleware.base import BaseHTTPMiddleware

from fastapi_startkit.exceptions import ExceptionHandler
Expand Down Expand Up @@ -64,7 +64,9 @@ def __init__(
self.load_providers()

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 All @@ -75,8 +77,6 @@ def register_providers(self):
config = {}
if isinstance(provider_data, tuple):
provider_class, config = provider_data
if callable(config):
config = config()
else:
provider_class = provider_data

Expand Down Expand Up @@ -129,8 +129,9 @@ def include_router(self, router: "APIRouter", **kwargs):
return self

# Add middleware
def add_middleware(self, middleware_class: type[Any], *args: Any, **kwargs: Any) -> None:
self._fastapi.add_middleware(middleware_class, *args, **kwargs)
def add_middleware(self, middleware_class: Type["BaseHTTPMiddleware"], **options):
self._fastapi.add_middleware(middleware_class, **options)
return self

# Add event handlers (startup/shutdown)
def add_event_handler(self, event_type: str, func: Callable[..., Any]):
Expand All @@ -143,7 +144,9 @@ 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 @@ -153,7 +156,9 @@ 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,7 +276,9 @@ 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
23 changes: 17 additions & 6 deletions fastapi_startkit/src/fastapi_startkit/commands/publish_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

from cleo.commands.command import Command
from cleo.helpers import option

from fastapi_startkit.helpers.string import Str

if TYPE_CHECKING:
Expand All @@ -16,7 +15,12 @@ class PublishCommand(Command):
description = "Publish provider config files into the project."

options = [
option("provider", "p", description="Provider name to publish (e.g. LogProvider, log_provider).", flag=False),
option(
"provider",
"p",
description="Provider name to publish (e.g. LogProvider, log_provider).",
flag=False,
),
]

def handle(self):
Expand All @@ -33,10 +37,14 @@ 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>")
self.line(
f"<error>No provider found matching '{provider_arg}'.</error>"
)
return
else:
resources = application.published_resources
Expand All @@ -47,10 +55,13 @@ def handle(self):

if os.path.exists(dest_path):
overwrite = self.confirm(
f" <comment>{destination}</comment> already exists. Overwrite?", default=False
f" <comment>{destination}</comment> already exists. Overwrite?",
default=False,
)
if not overwrite:
self.line(f" [{provider_key}] Skipped <comment>{destination}</comment>")
self.line(
f" [{provider_key}] Skipped <comment>{destination}</comment>"
)
continue

os.makedirs(os.path.dirname(dest_path), exist_ok=True)
Expand Down
7 changes: 4 additions & 3 deletions fastapi_startkit/src/fastapi_startkit/config/app.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import os
from dataclasses import dataclass, field

from dataclasses import field, dataclass
from fastapi_startkit.environment.environment import env


@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
@@ -1,4 +1,4 @@
from typing import TYPE_CHECKING, Type, TypeVar
from typing import TypeVar, Type, TYPE_CHECKING

if TYPE_CHECKING:
from fastapi_startkit.config import AppConfig
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from fastapi_startkit.loader import Loader

from ..exceptions import InvalidConfigurationSetup
from ..utils.structures import data
from ..exceptions import InvalidConfigurationLocation, InvalidConfigurationSetup


class Configuration:
Expand All @@ -27,7 +26,9 @@ 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 @@ -41,7 +42,9 @@ 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
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
from .Configuration import Configuration
from .helpers import config
from .Configuration import Configuration
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from ...providers import Provider

from ..Configuration import Configuration


Expand Down
1 change: 0 additions & 1 deletion fastapi_startkit/src/fastapi_startkit/console.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from cleo.application import Application as BaseApplication
from cleo.io.io import IO

from fastapi_startkit.application import Application


Expand Down
61 changes: 44 additions & 17 deletions fastapi_startkit/src/fastapi_startkit/container/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,14 @@ 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 @@ -141,7 +145,9 @@ 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 @@ -194,23 +200,28 @@ 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)
except TypeError as e:
raise ContainerError(str(e))
else:
for _, value in self.get_parameters(obj):
if type(value.annotation) in (str, int, dict, list, tuple) or value.annotation in (
if type(value.annotation) in (
str,
int,
dict,
list,
tuple,
):
) or value.annotation in (str, int, dict, list, tuple):
# Ignore any times a user is simply type hinting a parameter like (parameter:str or parameter:"str").
# In this case we don't want to resolve anything but we do want
# to insert any passing arguments we passed in
Expand Down Expand Up @@ -260,7 +271,9 @@ 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 @@ -292,15 +305,20 @@ 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 @@ -326,7 +344,10 @@ 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 @@ -341,7 +362,9 @@ 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 @@ -369,7 +392,9 @@ 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 @@ -478,7 +503,9 @@ 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 @@ -2,9 +2,8 @@

import os
import sys
from pathlib import Path

from dotenv import load_dotenv
from pathlib import Path


class LoadEnvironment:
Expand Down
Loading
Loading