diff --git a/fastapi_startkit/src/fastapi_startkit/collection/__init__.py b/fastapi_startkit/src/fastapi_startkit/collection/__init__.py
deleted file mode 100644
index 969ac3fe..00000000
--- a/fastapi_startkit/src/fastapi_startkit/collection/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from .collection import Collection
diff --git a/fastapi_startkit/src/fastapi_startkit/console/publish_command.py b/fastapi_startkit/src/fastapi_startkit/console/publish_command.py
index 736bdaa4..0c6452cc 100644
--- a/fastapi_startkit/src/fastapi_startkit/console/publish_command.py
+++ b/fastapi_startkit/src/fastapi_startkit/console/publish_command.py
@@ -4,7 +4,7 @@
from fastapi_startkit.console import Command
from cleo.helpers import option
-from fastapi_startkit.helpers.string import Str
+from fastapi_startkit.support import Str
if TYPE_CHECKING:
from fastapi_startkit.application import Application
diff --git a/fastapi_startkit/src/fastapi_startkit/exceptions.backup/DD.py b/fastapi_startkit/src/fastapi_startkit/exceptions.backup/DD.py
deleted file mode 100644
index 972c8cc9..00000000
--- a/fastapi_startkit/src/fastapi_startkit/exceptions.backup/DD.py
+++ /dev/null
@@ -1,38 +0,0 @@
-import inspect
-import warnings
-
-from .exceptions import DumpException
-
-
-warnings.warn(
- "DD class will be removed in Masonite 5. Please use Dump facade instead.",
- DeprecationWarning,
-)
-
-
-class DD:
- def __init__(self, container):
- self.app = container
-
- def die_and_dump(self, *args):
- """Dump all provided args and die, ie raise a DumpException."""
- self.dump(*args)
- raise DumpException
-
- def dump(self, *args):
- """Dump all provided args and let flow continue. This does not raise a DumpException."""
- print(
- inspect.stack()[1].function,
- inspect.stack()[1].filename,
- inspect.stack()[1].lineno,
- )
- if self.app.has("ObjDumpList"):
- dump_list = self.app.make("ObjDumpList")
- else:
- dump_list = []
- start = len(dump_list)
- for i, obj in enumerate(args):
- dump_name = f"ObjDump{start + i}"
- self.app.bind(dump_name, obj)
- dump_list.append(dump_name)
- self.app.bind("ObjDumpList", dump_list)
diff --git a/fastapi_startkit/src/fastapi_startkit/exceptions.backup/ExceptionHandler.py b/fastapi_startkit/src/fastapi_startkit/exceptions.backup/ExceptionHandler.py
deleted file mode 100644
index 43aefb4a..00000000
--- a/fastapi_startkit/src/fastapi_startkit/exceptions.backup/ExceptionHandler.py
+++ /dev/null
@@ -1,70 +0,0 @@
-class ExceptionHandler:
- def __init__(self, application, driver_config=None):
- self.application = application
- self.drivers = {}
- self.driver_config = driver_config or {}
- self.options = {}
-
- def set_options(self, options):
- self.options = options
- return self
-
- def add_driver(self, name, driver):
- self.drivers.update({name: driver})
-
- def set_configuration(self, config):
- self.driver_config = config
- return self
-
- def get_driver(self, name=None):
- if name is None:
- return self.drivers[self.driver_config.get("default")]
- return self.drivers[name]
-
- def get_config_options(self, driver=None):
- if driver is None:
- return self.driver_config[self.driver_config.get("default")]
-
- return self.driver_config.get(driver, {})
-
- 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)
-
- # add headers to response if any
- if hasattr(exception, "get_headers"):
- headers = exception.get_headers()
- response.with_headers(headers)
-
- # if an exception handler is registered for this exception, use it instead
- # add headers to response if any
- if hasattr(exception, "get_headers"):
- headers = exception.get_headers()
- response.with_headers(headers)
-
- if self.application.has(f"{exception.__class__.__name__}Handler"):
- 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"):
- return self.application.make("HttpExceptionHandler").handle(exception)
-
- # else fallback to an unknown exception that should be displayed as a 500 error
- exception.get_status = lambda: 500
- exception.get_response = lambda: str(exception) or "Unknown error"
- return self.application.make("HttpExceptionHandler").handle(exception)
-
- # handle exception in development mode with Exceptionite
- exceptionite = self.get_driver("exceptionite")
- exceptionite.start(exception)
- exceptionite.render("terminal")
-
- if request.accepts_json():
- return response.view(exceptionite.render("json"), status=500)
- else:
- return response.view(exceptionite.render("web"), status=500)
diff --git a/fastapi_startkit/src/fastapi_startkit/exceptions.backup/__init__.py b/fastapi_startkit/src/fastapi_startkit/exceptions.backup/__init__.py
deleted file mode 100644
index 590a19b7..00000000
--- a/fastapi_startkit/src/fastapi_startkit/exceptions.backup/__init__.py
+++ /dev/null
@@ -1,38 +0,0 @@
-from .ExceptionHandler import ExceptionHandler
-from .handlers.DumpExceptionHandler import DumpExceptionHandler
-from .handlers.HttpExceptionHandler import HttpExceptionHandler
-from .handlers.ModelNotFoundHandler import ModelNotFoundHandler
-from .DD import DD
-from .exceptions import (
- AuthorizationException,
- InvalidRouteCompileException,
- RouteMiddlewareNotFound,
- ContainerError,
- MissingContainerBindingNotFound,
- StrictContainerException,
- ResponseError,
- InvalidHTTPStatusCode,
- RequiredContainerBindingNotFound,
- ViewException,
- RouteNotFoundException,
- DumpException,
- InvalidSecretKey,
- InvalidCSRFToken,
- NotificationException,
- InvalidToken,
- ProjectLimitReached,
- ProjectProviderTimeout,
- ProjectProviderHttpError,
- ProjectTargetNotEmpty,
- MixFileNotFound,
- MixManifestNotFound,
- InvalidConfigurationLocation,
- InvalidConfigurationSetup,
- InvalidPackageName,
- LoaderNotFound,
- QueueException,
- AmbiguousError,
- MethodNotAllowedException,
- ModelNotFoundException,
- ThrottleRequestsException,
-)
diff --git a/fastapi_startkit/src/fastapi_startkit/exceptions.backup/exceptionite/blocks.py b/fastapi_startkit/src/fastapi_startkit/exceptions.backup/exceptionite/blocks.py
deleted file mode 100644
index 0236b09d..00000000
--- a/fastapi_startkit/src/fastapi_startkit/exceptions.backup/exceptionite/blocks.py
+++ /dev/null
@@ -1,101 +0,0 @@
-from exceptionite import Block
-
-from ... import __version__
-from ...helpers import optional
-from ...utils.str import get_controller_name
-
-
-def recursive_serializer(data):
- if isinstance(data, (int, bool, str, bytes)):
- return data
- elif isinstance(data, (list, tuple)):
- return [recursive_serializer(item) for item in data]
- elif isinstance(data, dict):
- return {key: recursive_serializer(val) for key, val in data.items()}
- elif callable(data):
- return str(data)
- elif hasattr(data, "serialize"):
- return data.serialize()
- else:
- return str(data)
-
-
-class AppBlock(Block):
- id = "application"
- name = "Application"
- icon = "DesktopComputerIcon"
- has_sections = True
-
- def build(self):
- request = self.handler.app.make("request")
- route = request.get_route()
-
- data = {
- "Info": {
- "Masonite Version": __version__,
- "Environment": self.handler.app.environment(),
- "Debug": self.handler.app.is_debug(),
- }
- }
-
- # add app route data
- if route:
- data.update(
- {
- "Route": {
- "Controller": get_controller_name(route.controller),
- "Name": route.get_name(),
- "Middlewares": route.get_middlewares(),
- }
- }
- )
-
- # add user route data
- user = request.user()
- if user:
- data.update(
- {
- "User": {
- "E-mail": optional(user).email,
- "ID": optional(user).id,
- }
- }
- )
-
- return data
-
-
-class RequestBlock(Block):
- id = "request"
- name = "Request"
- icon = "SwitchHorizontalIcon"
- has_sections = True
-
- def build(self):
- request = self.handler.app.make("request")
- # serialize inputs (e.g. in case of file)
- inputs = {}
- for name, value in request.all().items():
- inputs[name] = recursive_serializer(value)
- return {
- "Parameters": {
- "Path": request.get_path(),
- "Input": inputs or None,
- "Request Method": request.get_request_method(),
- },
- "Headers": request.header_bag.to_dict(),
- }
-
-
-class ConfigBlock(Block):
- id = "config"
- name = "Configuration"
- icon = "CogIcon"
- has_sections = True
-
- def build(self):
- data = {}
- for section, config_data in self.handler.app.make("config").all().items():
- section_name = section.title()
- data[section_name] = recursive_serializer(config_data)
- return data
diff --git a/fastapi_startkit/src/fastapi_startkit/exceptions.backup/exceptionite/controllers.py b/fastapi_startkit/src/fastapi_startkit/exceptions.backup/exceptionite/controllers.py
deleted file mode 100644
index 35fd676c..00000000
--- a/fastapi_startkit/src/fastapi_startkit/exceptions.backup/exceptionite/controllers.py
+++ /dev/null
@@ -1,13 +0,0 @@
-from ...request import Request
-from ...controllers import Controller
-from ...response import Response
-
-
-class ExceptioniteController(Controller):
- def run_action(self, request: Request, response: Response):
- handler = request.app.make("exception_handler").get_driver("exceptionite")
- data = handler.run_action(request.input("action_id"), request.input("options"))
- try:
- return response.json({"message": "ok", "data": data}, 200)
- except: # noqa: E722
- return response.json({"message": "An error happened", "data": data}, 400)
diff --git a/fastapi_startkit/src/fastapi_startkit/exceptions.backup/exceptionite/solutions.py b/fastapi_startkit/src/fastapi_startkit/exceptions.backup/exceptionite/solutions.py
deleted file mode 100644
index b5b8bf6d..00000000
--- a/fastapi_startkit/src/fastapi_startkit/exceptions.backup/exceptionite/solutions.py
+++ /dev/null
@@ -1,66 +0,0 @@
-class TableNotFound:
- def title(self):
- return "Table Not Found"
-
- def description(self):
- return "You are trying to make a query on a table that cannot be found. Check that :table migration exists and that migrations have been ran with 'python craft migrate' command."
-
- def regex(self):
- return r"no such table: (?P
(\w+))"
-
-
-class MissingCSRFToken:
- def title(self):
- return "Missing CSRF Token"
-
- def description(self):
- return "You are trying to make a sensitive request without providing a CSRF token. Your request might be vulnerable to Cross Site Request Forgery. To resolve this issue you should use {{ csrf_field }} in HTML forms or add X-CSRF-TOKEN header in AJAX requests."
-
- def regex(self):
- return r"Missing CSRF Token"
-
-
-class InvalidCSRFToken:
- def title(self):
- return "The session does not match the CSRF token"
-
- def description(self):
- return "Try clearing your cookies for the localhost domain in your browsers developer tools."
-
- def regex(self):
- return r"Invalid CSRF Token"
-
-
-class TemplateNotFound:
- def title(self):
- return "Template Not Found"
-
- def description(self):
- return """':template.html' view file has not been found in registered view locations. Please verify the spelling of the template and that it exists in locations declared in Kernel file. You can check
- available view locations with app.make('view.locations')."""
-
- def regex(self):
- return r"Template '(?P(\w+))' not found"
-
-
-class NoneResponse:
- def title(self):
- return "Response cannot be None"
-
- def description(self):
- return """Ensure that the controller method used in this request returned something. A controller method cannot return None or nothing.
- If you don't want to return a value you can return an empty string ''."""
-
- def regex(self):
- return r"Responses cannot be of type: None."
-
-
-class RouteMiddlewareNotFound:
- def title(self):
- return "Did you register the middleware key in your Kernel.py file?"
-
- def description(self):
- return "Check your Kernel.py file inside your 'route_middleware' attribute and look for a :middleware key"
-
- def regex(self):
- return r"Could not find the \'(?P(\w+))\' middleware key"
diff --git a/fastapi_startkit/src/fastapi_startkit/exceptions.backup/exceptionite/tabs.py b/fastapi_startkit/src/fastapi_startkit/exceptions.backup/exceptionite/tabs.py
deleted file mode 100644
index 2fd50fbc..00000000
--- a/fastapi_startkit/src/fastapi_startkit/exceptions.backup/exceptionite/tabs.py
+++ /dev/null
@@ -1,19 +0,0 @@
-from exceptionite import Tab
-
-
-class DumpsTab(Tab):
- id = "dumps"
- name = "Dumps"
- component = "DumpsTab"
- icon = "CodeIcon"
- advertise_content = True
- empty_msg = "Nothing dumped !"
-
- def build(self):
- dumps = self.handler.app.make("dumper").get_serialized_dumps()
- return {
- "dumps": dumps,
- }
-
- def has_content(self):
- return len(self.handler.app.make("dumper").get_dumps()) > 0
diff --git a/fastapi_startkit/src/fastapi_startkit/exceptions.backup/handlers/DumpExceptionHandler.py b/fastapi_startkit/src/fastapi_startkit/exceptions.backup/handlers/DumpExceptionHandler.py
deleted file mode 100644
index 3a75182b..00000000
--- a/fastapi_startkit/src/fastapi_startkit/exceptions.backup/handlers/DumpExceptionHandler.py
+++ /dev/null
@@ -1,102 +0,0 @@
-import inspect
-import os
-
-from ...utils.filesystem import get_module_dir
-
-
-def is_property(obj):
- return not inspect.ismethod(obj)
-
-
-def is_local(obj_name, obj):
- return (
- not obj_name.startswith("__")
- and not obj_name.endswith("__")
- and not type(obj).__name__ == "builtin_function_or_method"
- )
-
-
-def serialize_property(obj):
- if isinstance(obj, list):
- local_list = []
- for subobj in obj:
- local_list.append(serialize_property(subobj))
- return local_list
- elif isinstance(obj, dict):
- local_dict = {}
- for key, val in obj.items():
- local_dict.update({key: serialize_property(val)})
- return local_dict
- elif hasattr(obj, "serialize"):
- return obj.serialize()
- else:
- return str(obj)
-
-
-class DumpExceptionHandler:
- def __init__(self, application):
- self.application = application
-
- self.assets_path = os.path.join(get_module_dir(__file__), "../../templates/assets")
- self.styles = []
- self.scripts = []
-
- def add_style(self, file):
- with open(os.path.join(self.assets_path, file), "r") as f:
- self.styles.append(f.read())
-
- def add_script(self, file):
- with open(os.path.join(self.assets_path, file), "r") as f:
- self.scripts.append(f.read())
-
- def get_scripts(self):
- scripts_str = ""
- for script in self.scripts:
- scripts_str += f"\n"
- return scripts_str
-
- def get_styles(self):
- styles_str = ""
- for style in self.styles:
- styles_str += f"\n"
- return styles_str
-
- def handle(self, exception):
- dumps = []
- # for dump in self.application.make("dumper").get_dumps():
- # for obj_name, obj in dump.objects.items():
- # all_members = inspect.getmembers(obj, predicate=inspect.ismethod)
- # all_properties = inspect.getmembers(obj, predicate=is_property)
- # members = {
- # name: str(member)
- # for name, member in all_members
- # if is_local(name, member)
- # }
- # properties = {
- # name: serialize_property(prop)
- # for name, prop in all_properties
- # if is_local(name, prop)
- # }
- # dumps.append(
- # {
- # "name": obj_name,
- # "obj": str(obj),
- # "members": members,
- # "properties": properties,
- # }
- # )
- dumps = self.application.make("dumper").get_serialized_dumps()
- self.add_style("tailwind.css")
- self.add_style("github-dark.min.css")
- self.add_script("highlight.min.js")
-
- return self.application.make("response").view(
- self.application.make("view").render(
- "/masonite/templates/dump",
- {
- "styles": self.get_styles(),
- "scripts": self.get_scripts(),
- "dumps": dumps,
- },
- )
- )
diff --git a/fastapi_startkit/src/fastapi_startkit/exceptions.backup/handlers/HttpExceptionHandler.py b/fastapi_startkit/src/fastapi_startkit/exceptions.backup/handlers/HttpExceptionHandler.py
deleted file mode 100644
index a6f57385..00000000
--- a/fastapi_startkit/src/fastapi_startkit/exceptions.backup/handlers/HttpExceptionHandler.py
+++ /dev/null
@@ -1,26 +0,0 @@
-class HttpExceptionHandler:
- def __init__(self, application):
- self.application = application
-
- def handle(self, exception):
- status_code = exception.get_status()
- view_name = f"errors/{status_code}"
- response = self.application.make("response")
- request = self.application.make("request")
-
- if request.accepts_json():
- payload = {
- "status": exception.get_status(),
- "message": exception.get_response(),
- }
- return response.json(payload, status_code)
-
- # 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()}),
- status_code,
- )
- else:
- # Else render the exception without using template
- return response.view(exception.get_response(), status_code)
diff --git a/fastapi_startkit/src/fastapi_startkit/exceptions.backup/handlers/ModelNotFoundHandler.py b/fastapi_startkit/src/fastapi_startkit/exceptions.backup/handlers/ModelNotFoundHandler.py
deleted file mode 100644
index 68d53106..00000000
--- a/fastapi_startkit/src/fastapi_startkit/exceptions.backup/handlers/ModelNotFoundHandler.py
+++ /dev/null
@@ -1,11 +0,0 @@
-from ..exceptions import ModelNotFoundException
-
-
-class ModelNotFoundHandler:
- def __init__(self, application):
- self.application = application
-
- def handle(self, exception):
- 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)
diff --git a/fastapi_startkit/src/fastapi_startkit/facades/Auth.py b/fastapi_startkit/src/fastapi_startkit/facades/Auth.py
deleted file mode 100644
index ddeb848d..00000000
--- a/fastapi_startkit/src/fastapi_startkit/facades/Auth.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from .Facade import Facade
-
-
-class Auth(metaclass=Facade):
- key = "auth"
diff --git a/fastapi_startkit/src/fastapi_startkit/facades/Auth.pyi b/fastapi_startkit/src/fastapi_startkit/facades/Auth.pyi
deleted file mode 100644
index 514777ed..00000000
--- a/fastapi_startkit/src/fastapi_startkit/facades/Auth.pyi
+++ /dev/null
@@ -1,32 +0,0 @@
-from typing import TYPE_CHECKING, Any, Tuple, List
-
-if TYPE_CHECKING:
- from ..routes import Route
-
-class Auth:
- """Authentication facade."""
-
- def add_guard(name: str, guard: Any): ...
- def set_configuration(config: dict): ...
- def guard(guard: Any) -> "Auth": ...
- def get_guard(name: str = None) -> Any: ...
- def get_config_options(guard: Any = None) -> dict: ...
- def attempt(email: str, password: str, once: bool = False) -> Any: ...
- def attempt_by_id(user_id: int, once: bool = False) -> Any: ...
- def logout(self) -> "Auth":
- """Logout the current authenticated user."""
- ...
- def user(self) -> Any:
- """Get the current authenticated user."""
- ...
- def register(dictionary: dict) -> Any:
- """"""
- ...
- def password_reset(email: str) -> "Tuple[None, None]|Tuple[int,str]":
- """Reset password of the user with the given email."""
- ...
- def reset_password(password: str, token: str) -> bool:
- """Reset password of the user with the given authentication token."""
- ...
- @classmethod
- def routes(self) -> List[Route]: ...
diff --git a/fastapi_startkit/src/fastapi_startkit/facades/Inertia.py b/fastapi_startkit/src/fastapi_startkit/facades/Inertia.py
deleted file mode 100644
index 3e74a14e..00000000
--- a/fastapi_startkit/src/fastapi_startkit/facades/Inertia.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from .Facade import Facade
-
-
-class Inertia(metaclass=Facade):
- key = "inertia"
diff --git a/fastapi_startkit/src/fastapi_startkit/facades/__init__.py b/fastapi_startkit/src/fastapi_startkit/facades/__init__.py
index da562fb2..a8e5d064 100644
--- a/fastapi_startkit/src/fastapi_startkit/facades/__init__.py
+++ b/fastapi_startkit/src/fastapi_startkit/facades/__init__.py
@@ -7,7 +7,6 @@
from .Session import Session
from .View import View
from .Gate import Gate
-from .Auth import Auth
from .Config import Config
from .Loader import Loader
from .Notification import Notification
@@ -16,4 +15,3 @@
from .Cache import Cache
from .RateLimiter import RateLimiter
from .Broadcast import Broadcast
-from .Inertia import Inertia
diff --git a/fastapi_startkit/src/fastapi_startkit/loader/Loader.py b/fastapi_startkit/src/fastapi_startkit/loader/Loader.py
index c243b3cd..07b01cde 100644
--- a/fastapi_startkit/src/fastapi_startkit/loader/Loader.py
+++ b/fastapi_startkit/src/fastapi_startkit/loader/Loader.py
@@ -4,7 +4,6 @@
import pkgutil
from ..exceptions import LoaderNotFound
-from ..utils.str import as_filepath
from ..utils.structures import load
@@ -18,7 +17,7 @@ def get_modules(self, files_or_directories, raise_exception=False):
files_or_directories = [files_or_directories]
_modules = {}
- module_paths = list(map(as_filepath, files_or_directories))
+ module_paths = list(map(lambda p: p.replace(".", "/"), files_or_directories))
for module_loader, name, _ in pkgutil.iter_modules(module_paths):
module = load(
f"{module_loader.path}/{name}.py",
diff --git a/fastapi_startkit/src/fastapi_startkit/logging/channels/DailyChannel.py b/fastapi_startkit/src/fastapi_startkit/logging/channels/DailyChannel.py
index a263ab2e..06bb6669 100644
--- a/fastapi_startkit/src/fastapi_startkit/logging/channels/DailyChannel.py
+++ b/fastapi_startkit/src/fastapi_startkit/logging/channels/DailyChannel.py
@@ -1,8 +1,9 @@
-from ..factory import DriverFactory
-from fastapi_startkit.facades import Config
-from fastapi_startkit.utils.filesystem import make_directory
import os
+
+from fastapi_startkit.facades import Config
from .BaseChannel import BaseChannel
+from ..factory import DriverFactory
+from ..file import make_directory
class DailyChannel(BaseChannel):
diff --git a/fastapi_startkit/src/fastapi_startkit/logging/channels/SingleChannel.py b/fastapi_startkit/src/fastapi_startkit/logging/channels/SingleChannel.py
index 69b9513c..bb3667e5 100644
--- a/fastapi_startkit/src/fastapi_startkit/logging/channels/SingleChannel.py
+++ b/fastapi_startkit/src/fastapi_startkit/logging/channels/SingleChannel.py
@@ -1,6 +1,6 @@
from ..factory import DriverFactory
from fastapi_startkit.facades import Config
-from fastapi_startkit.utils.filesystem import make_directory
+from ..file import make_directory
from .BaseChannel import BaseChannel
diff --git a/fastapi_startkit/src/fastapi_startkit/logging/channels/SyslogChannel.py b/fastapi_startkit/src/fastapi_startkit/logging/channels/SyslogChannel.py
index a20c1a7d..c699c642 100644
--- a/fastapi_startkit/src/fastapi_startkit/logging/channels/SyslogChannel.py
+++ b/fastapi_startkit/src/fastapi_startkit/logging/channels/SyslogChannel.py
@@ -1,6 +1,6 @@
from ..factory import DriverFactory
from fastapi_startkit.facades import Config
-from fastapi_startkit.utils.filesystem import make_directory
+from ..file import make_directory
from .BaseChannel import BaseChannel
diff --git a/fastapi_startkit/src/fastapi_startkit/logging/drivers/LogTerminalDriver.py b/fastapi_startkit/src/fastapi_startkit/logging/drivers/LogTerminalDriver.py
index 3c929fc8..c263d7f5 100644
--- a/fastapi_startkit/src/fastapi_startkit/logging/drivers/LogTerminalDriver.py
+++ b/fastapi_startkit/src/fastapi_startkit/logging/drivers/LogTerminalDriver.py
@@ -1,8 +1,18 @@
-# from logging import Logger
+from .BaseDriver import BaseDriver
-from fastapi_startkit.utils.console import HasColoredOutput
-from .BaseDriver import BaseDriver
+class HasColoredOutput:
+ def success(self, message):
+ print("\033[92m {0} \033[0m".format(message))
+
+ def warning(self, message):
+ print("\033[93m {0} \033[0m".format(message))
+
+ def danger(self, message):
+ print("\033[91m {0} \033[0m".format(message))
+
+ def info(self, message):
+ return self.success(message)
class LogTerminalDriver(BaseDriver, HasColoredOutput):
diff --git a/fastapi_startkit/src/fastapi_startkit/logging/file.py b/fastapi_startkit/src/fastapi_startkit/logging/file.py
new file mode 100644
index 00000000..917b5dbd
--- /dev/null
+++ b/fastapi_startkit/src/fastapi_startkit/logging/file.py
@@ -0,0 +1,13 @@
+import os
+
+
+def make_directory(directory):
+ """Create a directory at the given path for a file if it does not exist"""
+ if not os.path.isfile(directory):
+ if not os.path.exists(os.path.dirname(directory)):
+ # Create the path to the model if it does not exist
+ os.makedirs(os.path.dirname(directory))
+
+ return True
+
+ return False
diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/collection/Collection.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/collection/Collection.py
index 0ef2c4c9..7cb2cc86 100644
--- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/collection/Collection.py
+++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/collection/Collection.py
@@ -1,4 +1,4 @@
-from fastapi_startkit.collection import Collection as BaseCollection
+from fastapi_startkit.support.collection import Collection as BaseCollection
class Collection(BaseCollection):
diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MakeMigrationCommand.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MakeMigrationCommand.py
index 0aa17efb..9f5db9c2 100644
--- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MakeMigrationCommand.py
+++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MakeMigrationCommand.py
@@ -3,7 +3,7 @@
import pathlib
from inflection import tableize
from cleo.helpers import argument, option
-from fastapi_startkit.helpers.string import Str
+from fastapi_startkit.support import Str
from fastapi_startkit.console import Command
diff --git a/fastapi_startkit/src/fastapi_startkit/providers/Provider.py b/fastapi_startkit/src/fastapi_startkit/providers/Provider.py
index 04059023..d2e6a831 100644
--- a/fastapi_startkit/src/fastapi_startkit/providers/Provider.py
+++ b/fastapi_startkit/src/fastapi_startkit/providers/Provider.py
@@ -1,6 +1,6 @@
from typing import TYPE_CHECKING
-from fastapi_startkit.helpers.string import Str
+from fastapi_startkit.support import Str
from fastapi_startkit.helpers.dataclass import Dataclass
diff --git a/fastapi_startkit/src/fastapi_startkit/utils/data/mime.types b/fastapi_startkit/src/fastapi_startkit/storage/data/mime.types
similarity index 100%
rename from fastapi_startkit/src/fastapi_startkit/utils/data/mime.types
rename to fastapi_startkit/src/fastapi_startkit/storage/data/mime.types
diff --git a/fastapi_startkit/src/fastapi_startkit/storage/drivers/local.py b/fastapi_startkit/src/fastapi_startkit/storage/drivers/local.py
index 2d677495..b932a0f0 100644
--- a/fastapi_startkit/src/fastapi_startkit/storage/drivers/local.py
+++ b/fastapi_startkit/src/fastapi_startkit/storage/drivers/local.py
@@ -5,7 +5,7 @@
from ..filestream import FileStream
from ..file import File
-from ...utils.filesystem import get_extension
+from ..helper import get_extension
class LocalDriver:
diff --git a/fastapi_startkit/src/fastapi_startkit/storage/drivers/s3.py b/fastapi_startkit/src/fastapi_startkit/storage/drivers/s3.py
index 3853ae99..a5ca66fe 100644
--- a/fastapi_startkit/src/fastapi_startkit/storage/drivers/s3.py
+++ b/fastapi_startkit/src/fastapi_startkit/storage/drivers/s3.py
@@ -2,7 +2,7 @@
import uuid
from ..file import File
-from ...utils.filesystem import get_extension
+from ..helper import get_extension
class S3Driver:
diff --git a/fastapi_startkit/src/fastapi_startkit/storage/file.py b/fastapi_startkit/src/fastapi_startkit/storage/file.py
index 6f506594..c19ddc7a 100644
--- a/fastapi_startkit/src/fastapi_startkit/storage/file.py
+++ b/fastapi_startkit/src/fastapi_startkit/storage/file.py
@@ -1,6 +1,6 @@
import hashlib
-from ..utils.filesystem import get_extension
+from .helper import get_extension
class File:
diff --git a/fastapi_startkit/src/fastapi_startkit/storage/filestream.py b/fastapi_startkit/src/fastapi_startkit/storage/filestream.py
index e0e8d708..7084cc3a 100644
--- a/fastapi_startkit/src/fastapi_startkit/storage/filestream.py
+++ b/fastapi_startkit/src/fastapi_startkit/storage/filestream.py
@@ -1,6 +1,6 @@
import os
-from ..utils.filesystem import get_extension
+from .helper import get_extension
class FileStream:
diff --git a/fastapi_startkit/src/fastapi_startkit/storage/helper.py b/fastapi_startkit/src/fastapi_startkit/storage/helper.py
new file mode 100644
index 00000000..08c62a49
--- /dev/null
+++ b/fastapi_startkit/src/fastapi_startkit/storage/helper.py
@@ -0,0 +1,33 @@
+import mimetypes
+import os
+import pathlib
+
+
+def get_module_dir(module_file):
+ return os.path.dirname(os.path.realpath(module_file))
+
+
+mimetypes.init([os.path.join(get_module_dir(__file__), "data/mime.types")])
+
+KNOWN_MIME_TYPES = mimetypes.types_map.keys()
+
+
+def get_extension(filepath: str, without_dot=False) -> str:
+ """Get a file extension from a filepath. If without_dot= True, the prefix will be removed from
+ the extension."""
+ extension_parts = pathlib.Path(filepath).suffixes
+ extension = ""
+ if extension_parts:
+ # try to join all the parts until only one part to check if it's a known extension
+ for i in range(len(extension_parts)):
+ try_extension = "".join(extension_parts[i:])
+ if try_extension in KNOWN_MIME_TYPES:
+ extension = try_extension
+ break
+ # if no known extension found, return the last part as the extension
+ if not extension:
+ extension = extension_parts[-1]
+
+ if without_dot:
+ extension = extension[1:]
+ return extension
diff --git a/fastapi_startkit/src/fastapi_startkit/support/__init__.py b/fastapi_startkit/src/fastapi_startkit/support/__init__.py
new file mode 100644
index 00000000..e8f3e426
--- /dev/null
+++ b/fastapi_startkit/src/fastapi_startkit/support/__init__.py
@@ -0,0 +1,4 @@
+from .collection import Collection, collect
+from .string import Str, Stringable
+
+__all__ = ["Collection", "collect", "Str", "Stringable"]
diff --git a/fastapi_startkit/src/fastapi_startkit/collection/collection.py b/fastapi_startkit/src/fastapi_startkit/support/collection.py
similarity index 98%
rename from fastapi_startkit/src/fastapi_startkit/collection/collection.py
rename to fastapi_startkit/src/fastapi_startkit/support/collection.py
index 84960a6a..3313c144 100644
--- a/fastapi_startkit/src/fastapi_startkit/collection/collection.py
+++ b/fastapi_startkit/src/fastapi_startkit/support/collection.py
@@ -555,7 +555,11 @@ def _make_comparison(self, a, b, op):
">": operator.gt,
">=": operator.ge,
}
- return operators[op](str(a), str(b))
+ # Use numeric comparison when both values are numeric
+ try:
+ return operators[op](float(a), float(b))
+ except (TypeError, ValueError):
+ return operators[op](str(a), str(b))
def __iter__(self):
for item in self._items:
@@ -611,3 +615,8 @@ def __get_items(cls, items):
items = items.all()
return items
+
+
+def collect(items=None) -> Collection:
+ """Shortcut to wrap items in a Collection."""
+ return Collection(items or [])
diff --git a/fastapi_startkit/src/fastapi_startkit/helpers/string.py b/fastapi_startkit/src/fastapi_startkit/support/string.py
similarity index 100%
rename from fastapi_startkit/src/fastapi_startkit/helpers/string.py
rename to fastapi_startkit/src/fastapi_startkit/support/string.py
diff --git a/fastapi_startkit/src/fastapi_startkit/utils/__init__.py b/fastapi_startkit/src/fastapi_startkit/utils/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/fastapi_startkit/src/fastapi_startkit/utils/collections.py b/fastapi_startkit/src/fastapi_startkit/utils/collections.py
deleted file mode 100644
index 2c5e2384..00000000
--- a/fastapi_startkit/src/fastapi_startkit/utils/collections.py
+++ /dev/null
@@ -1,543 +0,0 @@
-import json
-import random
-import operator
-from functools import reduce
-from dotty_dict import Dotty
-
-from .structures import data_get
-
-
-class Collection:
- """Wraps various data types to make working with them easier."""
-
- def __init__(self, items=None):
- self._items = items or []
- self.__appends__ = []
-
- def take(self, number: int):
- """Takes a specific number of results from the items.
-
- Arguments:
- number {integer} -- The number of results to take.
-
- Returns:
- int
- """
- if number < 0:
- return self[number:]
-
- return self[:number]
-
- def first(self, callback=None):
- """Takes the first result in the items.
-
- If a callback is given then the first result will be the result after the filter.
-
- Keyword Arguments:
- callback {callable} -- Used to filter the results before returning the first item. (default: {None})
-
- Returns:
- mixed -- Returns whatever the first item is.
- """
- filtered = self
- if callback:
- filtered = self.filter(callback)
- response = None
- if filtered:
- response = filtered[0]
- return response
-
- def last(self, callback=None):
- """Takes the last result in the items.
-
- If a callback is given then the last result will be the result after the filter.
-
- Keyword Arguments:
- callback {callable} -- Used to filter the results before returning the last item. (default: {None})
-
- Returns:
- mixed -- Returns whatever the last item is.
- """
- filtered = self
- if callback:
- filtered = self.filter(callback)
- return filtered[-1]
-
- def all(self):
- """Returns all the items.
-
- Returns:
- mixed -- Returns all items.
- """
- return self._items
-
- def avg(self, key=None):
- """Returns the average of the items.
-
- If a key is given it will return the average of all the values of the key.
-
- Keyword Arguments:
- key {string} -- The key to use to find the average of all the values of that key. (default: {None})
-
- Returns:
- int -- Returns the average.
- """
- result = 0
- items = self._get_value(key) or self._items
- try:
- result = sum(items) / len(items)
- except TypeError:
- pass
- return result
-
- def max(self, key=None):
- """Returns the average of the items.
-
- If a key is given it will return the average of all the values of the key.
-
- Keyword Arguments:
- key {string} -- The key to use to find the average of all the values of that key. (default: {None})
-
- Returns:
- int -- Returns the average.
- """
- result = 0
- items = self._get_value(key) or self._items
-
- try:
- return max(items)
- except (TypeError, ValueError):
- pass
- return result
-
- def chunk(self, size: int):
- """Chunks the items.
-
- Keyword Arguments:
- size {integer} -- The number of values in each chunk.
-
- Returns:
- int -- Returns the average.
- """
- items = []
- for i in range(0, self.count(), size):
- items.append(self[i : i + size])
- return self.__class__(items)
-
- def collapse(self):
- items = []
- for item in self:
- items += self.__get_items(item)
- return self.__class__(items)
-
- def contains(self, key, value=None):
- if value:
- return self.contains(lambda x: self._data_get(x, key) == value)
-
- if self._check_is_callable(key, raise_exception=False):
- return self.first(key) is not None
-
- return key in self
-
- def count(self):
- return len(self._items)
-
- def diff(self, items):
- items = self.__get_items(items)
- return self.__class__([x for x in self if x not in items])
-
- def each(self, callback):
- self._check_is_callable(callback)
-
- for k, v in enumerate(self):
- result = callback(v)
- if not result:
- break
- self[k] = result
-
- def every(self, callback):
- self._check_is_callable(callback)
- return all([callback(x) for x in self])
-
- def filter(self, callback):
- self._check_is_callable(callback)
- return self.__class__(list(filter(callback, self)))
-
- def flatten(self):
- def _flatten(items):
- if isinstance(items, dict):
- for v in items.values():
- for x in _flatten(v):
- yield x
- elif isinstance(items, list):
- for i in items:
- for j in _flatten(i):
- yield j
- else:
- yield items
-
- return self.__class__(list(_flatten(self._items)))
-
- def forget(self, *keys):
- keys = reversed(sorted(keys))
-
- for key in keys:
- del self[key]
-
- return self
-
- def for_page(self, page, number):
- return self.__class__(self[page:number])
-
- def get(self, key, default=None):
- try:
- return self[key]
- except IndexError:
- pass
-
- return self._value(default)
-
- def implode(self, glue=",", key=None):
- first = self.first()
- if not isinstance(first, str) and key:
- return glue.join(self.pluck(key))
- return glue.join([str(x) for x in self])
-
- def is_empty(self):
- return not self
-
- def map(self, callback):
- self._check_is_callable(callback)
- items = [callback(x) for x in self]
- return self.__class__(items)
-
- def map_into(self, cls, method=None, **kwargs):
- results = []
- for item in self:
- if method:
- results.append(getattr(cls, method)(item, **kwargs))
- else:
- results.append(cls(item))
-
- return self.__class__(results)
-
- def merge(self, items):
- if not isinstance(items, list):
- raise ValueError("Unable to merge uncompatible types")
-
- items = self.__get_items(items)
-
- self._items += items
- return self
-
- def pluck(self, value, key=None):
- if key:
- attributes = {}
- else:
- attributes = []
-
- if isinstance(self._items, dict):
- return Collection([self._items.get(value)])
-
- for item in self:
- if isinstance(item, dict):
- iterable = item.items()
- elif hasattr(item, "serialize"):
- iterable = item.serialize().items()
- else:
- iterable = self.all().items()
-
- for k, v in iterable:
- if k == value:
- if key:
- attributes[self._data_get(item, key)] = self._data_get(item, value)
- else:
- attributes.append(v)
-
- return Collection(attributes)
-
- def pop(self):
- last = self._items.pop()
- return last
-
- def prepend(self, value):
- self._items.insert(0, value)
- return self
-
- def pull(self, key):
- value = self.get(key)
- self.forget(key)
- return value
-
- def push(self, value):
- self._items.append(value)
-
- def put(self, key, value):
- self[key] = value
- return self
-
- def random(self, count=None):
- """Returns a random item of the collection."""
- collection_count = self.count()
- if collection_count == 0:
- return None
- elif count and count > collection_count:
- raise ValueError("count argument must be inferior to collection length.")
- elif count:
- self._items = random.sample(self._items, k=count)
- return self
- else:
- return random.choice(self._items)
-
- def reduce(self, callback, initial=0):
- return reduce(callback, self, initial)
-
- def reject(self, callback):
- self._check_is_callable(callback)
-
- items = self._get_value(callback) or self._items
- self._items = items
-
- def reverse(self):
- self._items = self[::-1]
-
- def serialize(self):
- def _serialize(item):
- if self.__appends__:
- item.set_appends(self.__appends__)
-
- if hasattr(item, "serialize"):
- return item.serialize()
- elif hasattr(item, "to_dict"):
- return item.to_dict()
- return item
-
- return list(map(_serialize, self))
-
- def add_relation(self, result=None):
- for model in self._items:
- model.add_relations(result or {})
-
- return self
-
- def shift(self):
- return self.pull(0)
-
- def sort(self, key=None):
- if key:
- self._items.sort(key=lambda x: x[key], reverse=False)
- return self
-
- self._items = sorted(self)
- return self
-
- def sum(self, key=None):
- result = 0
- items = self._get_value(key) or self._items
- try:
- result = sum(items)
- except TypeError:
- pass
- return result
-
- def to_json(self, **kwargs):
- return json.dumps(self.serialize(), **kwargs)
-
- def group_by(self, key):
-
- from itertools import groupby
-
- self.sort(key)
-
- new_dict = {}
-
- for k, v in groupby(self._items, key=lambda x: x[key]):
- new_dict.update({k: list(v)})
-
- return Collection(new_dict)
-
- def transform(self, callback):
- self._check_is_callable(callback)
- self._items = self._get_value(callback)
-
- def unique(self, key=None):
- if not key:
- items = list(set(self._items))
- return self.__class__(items)
-
- keys = set()
- items = []
- if isinstance(self.all(), dict):
- return self
-
- for item in self:
- if isinstance(item, dict):
- comparison = item.get(key)
- elif isinstance(item, str):
- comparison = item
- else:
- comparison = getattr(item, key)
- if comparison not in keys:
- items.append(item)
- keys.add(comparison)
-
- return self.__class__(items)
-
- def where(self, key, *args):
- op = "=="
- value = args[0]
-
- if len(args) >= 2:
- op = args[0]
- value = args[1]
-
- attributes = []
-
- for item in self._items:
- if isinstance(item, dict):
- comparison = item.get(key)
- else:
- comparison = getattr(item, key)
- if self._make_comparison(comparison, value, op):
- attributes.append(item)
-
- return self.__class__(attributes)
-
- def zip(self, items):
- items = self.__get_items(items)
- if not isinstance(items, list):
- raise ValueError("The 'items' parameter must be a list or a Collection")
-
- _items = []
- for x, y in zip(self, items):
- _items.append([x, y])
- return self.__class__(_items)
-
- def set_appends(self, appends):
- """
- Set the attributes that should be appended to the Collection.
-
- :rtype: list
- """
- self.__appends__ += appends
- return self
-
- def _get_value(self, key):
- if not key:
- return None
-
- items = []
- for item in self:
- if isinstance(key, str):
- if hasattr(item, key) or (key in item):
- items.append(getattr(item, key, item[key]))
- elif callable(key):
- result = key(item)
- if result:
- items.append(result)
- return items
-
- def _data_get(self, item, key, default=None):
- try:
- if isinstance(item, (list, tuple)):
- item = item[key]
- elif isinstance(item, (dict, Dotty)):
- item = data_get(item, key, default)
- elif isinstance(item, object):
- item = getattr(item, key)
- except (IndexError, AttributeError, KeyError, TypeError):
- return self._value(default)
-
- return item
-
- def _value(self, value):
- if callable(value):
- return value()
- return value
-
- def _check_is_callable(self, callback, raise_exception=True):
- if not callable(callback):
- if not raise_exception:
- return False
- raise ValueError("The 'callback' should be a function")
- return True
-
- def _make_comparison(self, a, b, op):
- operators = {
- "<": operator.lt,
- "<=": operator.le,
- "==": operator.eq,
- "!=": operator.ne,
- ">": operator.gt,
- ">=": operator.ge,
- }
- return operators[op](a, b)
-
- def __iter__(self):
- for item in self._items:
- yield item
-
- def __eq__(self, other):
- if isinstance(other, Collection):
- return other == other.all()
- return other == self._items
-
- def __getitem__(self, item):
- if isinstance(item, slice):
- return self.__class__(self._items[item])
-
- return self._items[item]
-
- def __setitem__(self, key, value):
- self._items[key] = value
-
- def __delitem__(self, key):
- del self._items[key]
-
- def __ne__(self, other):
- other = self.__get_items(other)
- return other != self._items
-
- def __len__(self):
- return len(self._items)
-
- def __le__(self, other):
- other = self.__get_items(other)
- return self._items <= other
-
- def __lt__(self, other):
- other = self.__get_items(other)
- return self._items < other
-
- def __ge__(self, other):
- other = self.__get_items(other)
- return self._items >= other
-
- def __gt__(self, other):
- other = self.__get_items(other)
- return self._items > other
-
- @classmethod
- def __get_items(cls, items):
- if isinstance(items, Collection):
- items = items.all()
-
- return items
-
-
-def collect(iterable):
- """Transform an iterable into a collection."""
- return Collection(iterable)
-
-
-def flatten(iterable):
- """Flatten all sub-iterables of an iterable structure (recursively)."""
- flat_list = []
- for item in iterable:
- if isinstance(item, list):
- for subitem in flatten(item):
- flat_list.append(subitem)
- else:
- flat_list.append(item)
-
- return flat_list
diff --git a/fastapi_startkit/src/fastapi_startkit/utils/console.py b/fastapi_startkit/src/fastapi_startkit/utils/console.py
deleted file mode 100644
index 52a4a58d..00000000
--- a/fastapi_startkit/src/fastapi_startkit/utils/console.py
+++ /dev/null
@@ -1,39 +0,0 @@
-class HasColoredOutput:
- """Add level-colored output print functions to a class."""
-
- def success(self, message):
- print("\033[92m {0} \033[0m".format(message))
-
- def warning(self, message):
- print("\033[93m {0} \033[0m".format(message))
-
- def danger(self, message):
- print("\033[91m {0} \033[0m".format(message))
-
- def info(self, message):
- return self.success(message)
-
-
-class AddCommandColors:
- """The default style set used by Cleo is defined here:
- https://github.com/sdispater/clikit/blob/master/src/clikit/formatter/default_style_set.py
- This mixin add method helper to output errors and warnings.
- """
-
- def error(self, text):
- """
- Write a string as information output.
-
- :param text: The line to write
- :type text: str
- """
- self.line(text, "error")
-
- def warning(self, text):
- """
- Write a string as information output.
-
- :param text: The line to write
- :type text: str
- """
- self.line(text, "c2")
diff --git a/fastapi_startkit/src/fastapi_startkit/utils/filesystem.py b/fastapi_startkit/src/fastapi_startkit/utils/filesystem.py
deleted file mode 100644
index b433fd7f..00000000
--- a/fastapi_startkit/src/fastapi_startkit/utils/filesystem.py
+++ /dev/null
@@ -1,100 +0,0 @@
-import os
-import platform
-import pathlib
-import mimetypes
-
-
-def make_directory(directory):
- """Create a directory at the given path for a file if it does not exist"""
- if not os.path.isfile(directory):
- if not os.path.exists(os.path.dirname(directory)):
- # Create the path to the model if it does not exist
- os.makedirs(os.path.dirname(directory))
-
- return True
-
- return False
-
-
-def file_exists(directory):
- """Create a directory at the given path for a file if it does not exist"""
- return os.path.exists(os.path.dirname(directory))
-
-
-def make_full_directory(directory):
- """Create all directories to the given path if they do not exist"""
- if not os.path.isfile(directory):
- if not os.path.exists(directory):
- # Create the path to the model if it does not exist
- os.makedirs(directory)
-
- return True
-
- return False
-
-
-def creation_date(path_to_file):
- """Try to get the date that a file was created, falling back to when it was
- last modified if that isn't possible.
- """
- if platform.system() == "Windows":
- return os.path.getctime(path_to_file)
- else:
- stat = os.stat(path_to_file)
- try:
- return stat.st_birthtime
- except AttributeError:
- # We're probably on Linux. No easy way to get creation dates here,
- # so we'll settle for when its content was last modified.
- return stat.st_mtime
-
-
-def modified_date(path_to_file):
- if platform.system() == "Windows":
- return os.path.getmtime(path_to_file)
- else:
- stat = os.stat(path_to_file)
- try:
- return stat.st_mtime
- except AttributeError:
- # We're probably on Linux. No easy way to get creation dates here,
- # so we'll settle for when its content was last modified.
- return 0
-
-
-def render_stub_file(stub_file, name):
- """Read stub file, replace placeholders and return content."""
- with open(stub_file, "r") as f:
- content = f.read()
- content = content.replace("__class__", name)
- return content
-
-
-def get_module_dir(module_file):
- return os.path.dirname(os.path.realpath(module_file))
-
-
-mimetypes.init([os.path.join(get_module_dir(__file__), "data/mime.types")])
-
-KNOWN_MIME_TYPES = mimetypes.types_map.keys()
-
-
-def get_extension(filepath: str, without_dot=False) -> str:
- """Get file extension from a filepath. If without_dot=True the . prefix will be removed from
- the extension."""
- extension_parts = pathlib.Path(filepath).suffixes
- extension = ""
- if extension_parts:
- # try to join all the parts until only one part to check if it's a known extension
- for i in range(len(extension_parts)):
- try_extension = "".join(extension_parts[i:])
- if try_extension in KNOWN_MIME_TYPES:
- extension = try_extension
- break
- # if no known extension found, return the last part as the extension
- if not extension:
- extension = extension_parts[-1]
-
- if without_dot:
- extension = extension[1:]
- return extension
diff --git a/fastapi_startkit/src/fastapi_startkit/utils/http.py b/fastapi_startkit/src/fastapi_startkit/utils/http.py
deleted file mode 100644
index 06e1e1d0..00000000
--- a/fastapi_startkit/src/fastapi_startkit/utils/http.py
+++ /dev/null
@@ -1,100 +0,0 @@
-"""Helpers for working with HTTP."""
-
-HTTP_STATUS_CODES = {
- 100: "100 Continue",
- 101: "101 Switching Protocol",
- 102: "102 Processing",
- 103: "Early Hints",
- 200: "200 OK",
- 201: "201 Created",
- 202: "202 Accepted",
- 203: "203 Non-Authoritative Information",
- 204: "204 No Content",
- 205: "205 Reset Content",
- 206: "206 Partial Content",
- 207: "207 Multi-Status",
- 208: "208 Multi-Status",
- 226: "226 IM Used",
- 300: "300 Multiple Choice",
- 301: "301 Moved Permanently",
- 302: "302 Found",
- 303: "303 See Other",
- 304: "304 Not Modified",
- 307: "307 Temporary Redirect",
- 308: "308 Permanent Redirect",
- 400: "400 Bad Request",
- 401: "401 Unauthorized",
- 402: "402 Payment Required",
- 403: "403 Forbidden",
- 404: "404 Not Found",
- 405: "405 Method Not Allowed",
- 406: "406 Not Acceptable",
- 407: "407 Proxy Authentication Required",
- 408: "408 Request Timeout",
- 409: "409 Conflict",
- 410: "410 Gone",
- 411: "411 Length Required",
- 412: "412 Precondition Failed",
- 413: "413 Payload Too Large",
- 414: "414 URI Too Long",
- 415: "415 Unsupported Media Type",
- 416: "416 Requested Range Not Satisfiable",
- 417: "417 Expectation Failed",
- 418: "418 I'm a teapot",
- 421: "421 Misdirected Request",
- 422: "422 Unprocessable Entity",
- 423: "423 Locked",
- 424: "424 Failed Dependency",
- 425: "425 Too Early",
- 426: "426 Upgrade Required",
- 428: "428 Precondition Required",
- 429: "429 Too Many Requests",
- 431: "431 Request Header Fields Too Large",
- 451: "451 Unavailable For Legal Reasons",
- 500: "500 Internal Server Error",
- 501: "501 Not Implemented",
- 502: "502 Bad Gateway",
- 503: "503 Service Unavailable",
- 504: "504 Gateway Timeout",
- 505: "505 HTTP Version Not Supported",
- 506: "506 Variant Also Negotiates",
- 507: "507 Insufficient Storage",
- 508: "508 Loop Detected",
- 510: "510 Not Extended",
- 511: "511 Network Authentication Required",
-}
-
-
-def generate_wsgi(wsgi={}, path="/", query_string="", method="GET"):
- """Generate the WSGI environment dictionary that we receive from a HTTP request."""
- import io
-
- data = {
- "wsgi.version": (1, 0),
- "wsgi.multithread": False,
- "wsgi.multiprocess": True,
- "wsgi.run_once": False,
- "wsgi.input": io.BytesIO(),
- "SERVER_SOFTWARE": "gunicorn/19.7.1",
- "REQUEST_METHOD": method,
- "QUERY_STRING": query_string,
- "RAW_URI": path,
- "SERVER_PROTOCOL": "HTTP/1.1",
- "HTTP_HOST": "127.0.0.1:8000",
- "HTTP_ACCEPT": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
- "HTTP_UPGRADE_INSECURE_REQUESTS": "1",
- "HTTP_COOKIE": "",
- "HTTP_USER_AGENT": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/604.4.7 (KHTML, like Gecko) Version/11.0.2 Safari/604.4.7",
- "HTTP_ACCEPT_LANGUAGE": "en-us",
- "HTTP_ACCEPT_ENCODING": "gzip, deflate",
- "HTTP_CONNECTION": "keep-alive",
- "wsgi.url_scheme": "http",
- "REMOTE_ADDR": "127.0.0.1",
- "REMOTE_PORT": "62241",
- "SERVER_NAME": "127.0.0.1",
- "SERVER_PORT": "8000",
- "PATH_INFO": path,
- "SCRIPT_NAME": "",
- }
- data.update(wsgi)
- return data
diff --git a/fastapi_startkit/src/fastapi_startkit/utils/location.py b/fastapi_startkit/src/fastapi_startkit/utils/location.py
deleted file mode 100644
index 326475ab..00000000
--- a/fastapi_startkit/src/fastapi_startkit/utils/location.py
+++ /dev/null
@@ -1,91 +0,0 @@
-"""Helpers to resolve absolute paths to the different app resources using a configured
-location."""
-
-from os.path import join, abspath
-
-from .str import as_filepath
-
-
-def _build_path(location_key, relative_path, absolute):
- from wsgi import application
-
- relative_dir = join(as_filepath(application.make(location_key)), relative_path)
- return abspath(relative_dir) if absolute else relative_dir
-
-
-def base_path(relative_path=""):
- """Build the absolute path to the project root directory or build the absolute path to a
- given file relative to the project root directory."""
- return abspath(relative_path)
-
-
-def views_path(relative_path="", absolute=True):
- """Build the absolute path to the project views directory or build the absolute path to a given
- file relative to the project views directory.
-
- The relative path can be returned instead by setting absolute=False."""
- return _build_path("views.location", relative_path, absolute)
-
-
-def controllers_path(relative_path="", absolute=True):
- """Build the absolute path to the project controllers directory or build the absolute path to a given
- file relative to the project controllers directory.
-
- The relative path can be returned instead by setting absolute=False."""
- return _build_path("controllers.location", relative_path, absolute)
-
-
-def mailables_path(relative_path="", absolute=True):
- """Build the absolute path to the project controllers directory or build the absolute path to a given
- file relative to the project controllers directory.
-
- The relative path can be returned instead by setting absolute=False."""
- return _build_path("mailables.location", relative_path, absolute)
-
-
-def config_path(relative_path="", absolute=True):
- """Build the absolute path to the project configuration directory or build the absolute path to a given
- file relative to the project configuration directory.
-
- The relative path can be returned instead by setting absolute=False."""
- return _build_path("config.location", relative_path, absolute)
-
-
-def migrations_path(relative_path="", absolute=True):
- """Build the absolute path to the project migrations directory or build the absolute path to a given
- file relative to the project migrations directory.
-
- The relative path can be returned instead by setting absolute=False."""
- return _build_path("migrations.location", relative_path, absolute)
-
-
-def seeds_path(relative_path="", absolute=True):
- """Build the absolute path to the project seeds directory or build the absolute path to a given
- file relative to the project seeds directory.
-
- The relative path can be returned instead by setting absolute=False."""
- return _build_path("seeds.location", relative_path, absolute)
-
-
-def jobs_path(relative_path="", absolute=True):
- """Build the absolute path to the project jobs directory or build the absolute path to a given
- file relative to the project jobs directory.
-
- The relative path can be returned instead by setting absolute=False."""
- return _build_path("jobs.location", relative_path, absolute)
-
-
-def resources_path(relative_path="", absolute=True):
- """Build the absolute path to the project resources directory or build the absolute path to a given
- file relative to the project resources directory.
-
- The relative path can be returned instead by setting absolute=False."""
- return _build_path("resources.location", relative_path, absolute)
-
-
-def models_path(relative_path="", absolute=True):
- """Build the absolute path to the project models directory or build the absolute path to a given
- file relative to the project models directory.
-
- The relative path can be returned instead by setting absolute=False."""
- return _build_path("models.location", relative_path, absolute)
diff --git a/fastapi_startkit/src/fastapi_startkit/utils/str.py b/fastapi_startkit/src/fastapi_startkit/utils/str.py
deleted file mode 100644
index 07eeaa5e..00000000
--- a/fastapi_startkit/src/fastapi_startkit/utils/str.py
+++ /dev/null
@@ -1,116 +0,0 @@
-"""String generators and helpers"""
-
-import random
-import string
-from urllib import parse
-from typing import Any
-
-
-def random_string(length=4):
- """Generate a random string based on the given length.
-
- Keyword Arguments:
- length {int} -- The amount of the characters to generate (default: {4})
-
- Returns:
- string
- """
- return "".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(length))
-
-
-def modularize(file_path, suffix=".py"):
- """Transforms a file path to a dotted path. On UNIX paths contains / and on Windows \\.
-
- Keyword Arguments:
- file_path {str} -- A file path such app/controllers
-
- Returns:
- value {str} -- a dotted path such as app.controllers
- """
- # if the file had the .py extension remove it as it's not needed for a module
- return removesuffix(file_path.replace("/", ".").replace("\\", "."), suffix)
-
-
-def as_filepath(dotted_path):
- """Inverse of modularize, transforms a dotted path to a file path (with /).
-
- Keyword Arguments:
- dotted_path {str} -- A dotted path such app.controllers
-
- Returns:
- value {str} -- a file path such as app/controllers
- """
- return dotted_path.replace(".", "/")
-
-
-def removeprefix(string, prefix):
- """Implementation of str.removeprefix() function available for Python versions lower than 3.9."""
- if string.startswith(prefix):
- return string[len(prefix) :]
- else:
- return string
-
-
-def removesuffix(string, suffix):
- """Implementation of str.removesuffix() function available for Python versions lower than 3.9."""
- if suffix and string.endswith(suffix):
- return string[: -len(suffix)]
- else:
- return string
-
-
-def match(string: str, ref_string: str) -> str:
- """Check if a given string matches a reference string. Wildcard '*' can be used at start, end
- or middle of the string."""
- if ref_string.startswith("*"):
- ref_string = ref_string.replace("*", "")
- return string.endswith(ref_string)
- elif ref_string.endswith("*"):
- ref_string = ref_string.replace("*", "")
- return string.startswith(ref_string)
- elif "*" in ref_string:
- split_search = ref_string.split("*")
- return string.startswith(split_search[0]) and string.endswith(split_search[1])
- else:
- return ref_string == string
-
-
-def add_query_params(url: str, query_params: dict) -> str:
- """Add query params dict to a given url (which can already contain some query parameters)."""
- path_result = parse.urlsplit(url)
-
- base_url = f"{path_result.scheme}://{path_result.hostname}" if path_result.hostname else ""
- base_path = path_result.path
-
- # parse existing query parameters if any
- existing_query_params = dict(parse.parse_qsl(path_result.query))
- all_query_params = {**existing_query_params, **query_params}
-
- # add query parameters to url if any
- if all_query_params:
- base_path += "?" + parse.urlencode(all_query_params)
-
- result_url = f"{base_url}{base_path}"
-
- # add fragment if exists
- if path_result.fragment:
- result_url = f"{result_url}#{path_result.fragment}"
-
- return result_url
-
-
-def get_controller_name(controller: "str|Any") -> str:
- """Get a controller string name from a controller argument used in routes."""
- # controller is a class or class.method
- if hasattr(controller, "__qualname__"):
- if "." in controller.__qualname__:
- controller_str = controller.__qualname__.replace(".", "@")
- else:
- controller_str = f"{controller.__qualname__}@__call__"
- # controller is an instance, so the method will automatically be __call__
- elif not isinstance(controller, str):
- controller_str = f"{controller.__class__.__qualname__}@__call__"
- # controller is anything else: "Controller@method"
- else:
- controller_str = str(controller)
- return controller_str
diff --git a/fastapi_startkit/src/fastapi_startkit/utils/time.py b/fastapi_startkit/src/fastapi_startkit/utils/time.py
deleted file mode 100644
index ed91cc27..00000000
--- a/fastapi_startkit/src/fastapi_startkit/utils/time.py
+++ /dev/null
@@ -1,59 +0,0 @@
-"""Time related helpers"""
-
-import pendulum
-
-
-def cookie_expire_time(str_time):
- """Take a string like 1 month or 5 minutes and returns a datetime formatted with cookie format.
-
- Arguments:
- str_time {string} -- Could be values like 1 second or 3 minutes
-
- Returns:
- str -- Cookie expiration time (Thu, 21 Oct 2021 07:28:00)
- """
- instance = parse_human_time(str_time)
- return instance.format("ddd, DD MMM YYYY HH:mm:ss")
-
-
-def parse_human_time(str_time):
- """Take a string like 1 month or 5 minutes and returns a pendulum instance.
-
- Arguments:
- str_time {string} -- Could be values like 1 second or 3 minutes
-
- Returns:
- pendulum -- Returns Pendulum instance
- """
- if str_time == "now":
- return pendulum.now("GMT")
-
- if str_time != "expired":
- number = int(str_time.split(" ")[0])
- length = str_time.split(" ")[1]
-
- if length in ("second", "seconds"):
- return pendulum.now("GMT").add(seconds=number)
- elif length in ("minute", "minutes"):
- return pendulum.now("GMT").add(minutes=number)
- elif length in ("hour", "hours"):
- return pendulum.now("GMT").add(hours=number)
- elif length in ("day", "days"):
- return pendulum.now("GMT").add(days=number)
- elif length in ("week", "weeks"):
- return pendulum.now("GMT").add(weeks=number)
- elif length in ("month", "months"):
- return pendulum.now("GMT").add(months=number)
- elif length in ("year", "years"):
- return pendulum.now("GMT").add(years=number)
-
- return None
- else:
- return pendulum.now("GMT").subtract(years=20)
-
-
-def migration_timestamp():
- """Return current time formatted for creating migration filenames.
- Example: 2021_01_09_043202
- """
- return pendulum.now().format("YYYY_MM_DD_HHmmss")
diff --git a/fastapi_startkit/tests/facades/test_facades.py b/fastapi_startkit/tests/facades/test_facades.py
index e732a6df..a5533807 100644
--- a/fastapi_startkit/tests/facades/test_facades.py
+++ b/fastapi_startkit/tests/facades/test_facades.py
@@ -146,11 +146,6 @@ def test_config_does_not_bleed_between_apps(self, tmp_path):
class TestFacadeKeyAttributes:
- def test_auth_facade_has_key(self):
- from fastapi_startkit.facades import Auth
-
- assert hasattr(Auth, "key") or Auth.__class__.__name__ in ("type", "Facade")
-
def test_hash_facade_key(self):
try:
from fastapi_startkit.facades import Hash
diff --git a/fastapi_startkit/src/fastapi_startkit/exceptions.backup/exceptionite/__init__.py b/fastapi_startkit/tests/utils/__init__.py
similarity index 100%
rename from fastapi_startkit/src/fastapi_startkit/exceptions.backup/exceptionite/__init__.py
rename to fastapi_startkit/tests/utils/__init__.py
diff --git a/fastapi_startkit/tests/utils/test_collections.py b/fastapi_startkit/tests/utils/test_collections.py
new file mode 100644
index 00000000..3924f40f
--- /dev/null
+++ b/fastapi_startkit/tests/utils/test_collections.py
@@ -0,0 +1,215 @@
+"""Tests for Collection utility class (task #15)."""
+
+import pytest
+
+from fastapi_startkit.support import Collection, collect
+
+
+class TestCollectionBasics:
+ def test_empty_collection(self):
+ c = Collection()
+ assert c.count() == 0
+ assert c.is_empty()
+
+ def test_count(self):
+ c = Collection([1, 2, 3])
+ assert c.count() == 3
+
+ def test_all_returns_items(self):
+ c = Collection([10, 20])
+ assert c.all() == [10, 20]
+
+ def test_iteration(self):
+ items = [1, 2, 3]
+ c = Collection(items)
+ assert list(c) == items
+
+ def test_getitem(self):
+ c = Collection(["a", "b", "c"])
+ assert c[0] == "a"
+ assert c[-1] == "c"
+
+
+class TestCollectionFirstLast:
+ def test_first_without_callback(self):
+ assert Collection([5, 6, 7]).first() == 5
+
+ def test_first_with_callback(self):
+ c = Collection([1, 2, 3, 4])
+ result = c.first(lambda x: x > 2)
+ assert result == 3
+
+ def test_last_without_callback(self):
+ assert Collection([1, 2, 3]).last() == 3
+
+ def test_last_with_callback(self):
+ c = Collection([1, 2, 3, 4])
+ result = c.last(lambda x: x < 3)
+ assert result == 2
+
+ def test_first_returns_none_for_empty(self):
+ assert Collection([]).first() is None
+
+
+class TestCollectionMap:
+ def test_map_transforms_items(self):
+ c = Collection([1, 2, 3])
+ result = c.map(lambda x: x * 2)
+ assert result.all() == [2, 4, 6]
+
+ def test_map_returns_new_collection(self):
+ c = Collection([1, 2, 3])
+ result = c.map(lambda x: x)
+ assert isinstance(result, Collection)
+
+
+class TestCollectionFilter:
+ def test_filter_keeps_matching_items(self):
+ c = Collection([1, 2, 3, 4, 5])
+ result = c.filter(lambda x: x % 2 == 0)
+ assert result.all() == [2, 4]
+
+ def test_filter_raises_on_non_callable(self):
+ with pytest.raises(ValueError):
+ Collection([1, 2]).filter("not a callable")
+
+
+class TestCollectionPluck:
+ def test_pluck_values_from_dicts(self):
+ c = Collection([{"name": "Alice"}, {"name": "Bob"}])
+ result = c.pluck("name")
+ assert result.all() == ["Alice", "Bob"]
+
+ def test_pluck_with_key(self):
+ c = Collection([{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}])
+ result = c.pluck("name", "id")
+ assert result.all() == {1: "Alice", 2: "Bob"}
+
+
+class TestCollectionChunk:
+ def test_chunk_even(self):
+ c = Collection([1, 2, 3, 4])
+ chunks = c.chunk(2)
+ result = [ch.all() for ch in chunks]
+ assert result == [[1, 2], [3, 4]]
+
+ def test_chunk_uneven(self):
+ c = Collection([1, 2, 3, 4, 5])
+ chunks = c.chunk(2)
+ result = [ch.all() for ch in chunks]
+ assert result == [[1, 2], [3, 4], [5]]
+
+ def test_chunk_size_larger_than_collection(self):
+ c = Collection([1, 2])
+ chunks = c.chunk(10)
+ result = [ch.all() for ch in chunks]
+ assert result == [[1, 2]]
+
+
+class TestCollectionGroupBy:
+ def test_group_by_key(self):
+ c = Collection(
+ [
+ {"category": "A", "val": 1},
+ {"category": "B", "val": 2},
+ {"category": "A", "val": 3},
+ ]
+ )
+ result = c.group_by("category")
+ grouped = result.all()
+ assert "A" in grouped
+ assert "B" in grouped
+ assert len(grouped["A"]) == 2
+ assert len(grouped["B"]) == 1
+
+
+class TestCollectionSum:
+ def test_sum_numbers(self):
+ assert Collection([1, 2, 3]).sum() == 6
+
+ def test_sum_key(self):
+ c = Collection([{"price": 10}, {"price": 20}])
+ assert c.sum("price") == 30
+
+ def test_sum_empty(self):
+ assert Collection([]).sum() == 0
+
+
+class TestCollectionImplode:
+ def test_implode_strings(self):
+ result = Collection(["a", "b", "c"]).implode(", ")
+ assert result == "a, b, c"
+
+ def test_implode_numbers(self):
+ result = Collection([1, 2, 3]).implode("-")
+ assert result == "1-2-3"
+
+
+class TestCollectionMerge:
+ def test_merge_adds_items(self):
+ c = Collection([1, 2])
+ c.merge([3, 4])
+ assert c.all() == [1, 2, 3, 4]
+
+ def test_merge_raises_on_non_list(self):
+ with pytest.raises(ValueError):
+ Collection([1]).merge("not a list")
+
+
+class TestCollectionUnique:
+ def test_unique_primitives(self):
+ result = Collection([1, 2, 2, 3, 3]).unique()
+ assert len(result.all()) == 3
+
+ def test_unique_by_key(self):
+ c = Collection([{"id": 1, "x": "a"}, {"id": 1, "x": "b"}, {"id": 2, "x": "c"}])
+ result = c.unique("id")
+ assert result.count() == 2
+
+
+class TestCollectionWhere:
+ def test_where_equals(self):
+ c = Collection([{"age": 10}, {"age": 20}, {"age": 10}])
+ result = c.where("age", 10)
+ assert result.count() == 2
+
+ def test_where_greater_than(self):
+ c = Collection([{"n": 1}, {"n": 5}, {"n": 10}])
+ result = c.where("n", ">", 4)
+ assert result.count() == 2
+
+
+class TestCollectionContains:
+ def test_contains_primitive(self):
+ c = Collection([1, 2, 3])
+ assert c.contains(2) is True
+ assert c.contains(99) is False
+
+ def test_contains_with_callback(self):
+ c = Collection([1, 2, 3])
+ assert c.contains(lambda x: x > 2) is True
+ assert c.contains(lambda x: x > 10) is False
+
+
+class TestCollect:
+ def test_collect_returns_collection(self):
+ result = collect([1, 2, 3])
+ assert isinstance(result, Collection)
+ assert result.all() == [1, 2, 3]
+
+
+class TestFlatten:
+ def test_flatten_nested_lists(self):
+ result = Collection([[1, 2], [3, [4, 5]]]).flatten()
+ assert result.all() == [1, 2, 3, 4, 5]
+
+ def test_flatten_already_flat(self):
+ result = Collection([1, 2, 3]).flatten()
+ assert result.all() == [1, 2, 3]
+
+ def test_flatten_empty(self):
+ assert Collection([]).flatten().all() == []
+
+ def test_flatten_deeply_nested(self):
+ result = Collection([[[1]], [2, [3]]]).flatten()
+ assert result.all() == [1, 2, 3]