fastapi-refine 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,22 @@
1
+ """fastapi-refine: FastAPI integration for Refine simple-rest data provider."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ from fastapi_refine.core import FilterConfig, FilterField, PaginationConfig, SortConfig
6
+ from fastapi_refine.dependencies import RefineQuery, RefineResponse, refine_query, refine_response
7
+ from fastapi_refine.hooks import HookContext, RefineHooks
8
+ from fastapi_refine.routers import RefineCRUDRouter
9
+
10
+ __all__ = [
11
+ "FilterConfig",
12
+ "FilterField",
13
+ "SortConfig",
14
+ "PaginationConfig",
15
+ "RefineQuery",
16
+ "RefineResponse",
17
+ "refine_query",
18
+ "refine_response",
19
+ "HookContext",
20
+ "RefineHooks",
21
+ "RefineCRUDRouter",
22
+ ]
@@ -0,0 +1,27 @@
1
+ """Core modules for fastapi-refine."""
2
+
3
+ from fastapi_refine.core.query import (
4
+ parse_bool,
5
+ parse_filters,
6
+ parse_sorters,
7
+ parse_uuid,
8
+ resolve_pagination,
9
+ )
10
+ from fastapi_refine.core.types import (
11
+ FilterConfig,
12
+ FilterField,
13
+ PaginationConfig,
14
+ SortConfig,
15
+ )
16
+
17
+ __all__ = [
18
+ "FilterConfig",
19
+ "FilterField",
20
+ "SortConfig",
21
+ "PaginationConfig",
22
+ "parse_bool",
23
+ "parse_filters",
24
+ "parse_sorters",
25
+ "parse_uuid",
26
+ "resolve_pagination",
27
+ ]
@@ -0,0 +1,187 @@
1
+ """Query parsing logic for Refine simple-rest conventions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import uuid
6
+ from typing import Any
7
+
8
+ from sqlalchemy import ColumnElement, or_
9
+ from starlette.datastructures import QueryParams
10
+
11
+ __all__ = [
12
+ "parse_filters",
13
+ "parse_sorters",
14
+ "resolve_pagination",
15
+ "parse_bool",
16
+ "parse_uuid",
17
+ ]
18
+
19
+ IGNORED_QUERY_KEYS = {"_start", "_end", "_sort", "_order", "id", "skip", "limit"}
20
+
21
+
22
+ def parse_bool(value: str) -> bool:
23
+ """Parse boolean from string.
24
+
25
+ Supports: 1/0, true/false, t/f, yes/y, no/n (case-insensitive).
26
+ """
27
+ lowered = value.strip().lower()
28
+ if lowered in {"1", "true", "t", "yes", "y"}:
29
+ return True
30
+ if lowered in {"0", "false", "f", "no", "n"}:
31
+ return False
32
+ raise ValueError(f"Invalid boolean value: {value}")
33
+
34
+
35
+ def parse_uuid(value: str) -> uuid.UUID:
36
+ """Parse UUID from string."""
37
+ return uuid.UUID(value)
38
+
39
+
40
+ def split_filter_key(key: str) -> tuple[str, str]:
41
+ """Split filter key into field name and operator.
42
+
43
+ Examples:
44
+ "name" -> ("name", "eq")
45
+ "age_gte" -> ("age", "gte")
46
+ "title_like" -> ("title", "like")
47
+ """
48
+ for suffix in ("_ne", "_gte", "_lte", "_like"):
49
+ if key.endswith(suffix):
50
+ return key[: -len(suffix)], suffix[1:]
51
+ return key, "eq"
52
+
53
+
54
+ def parse_filters(
55
+ query_params: QueryParams,
56
+ *,
57
+ filter_fields: dict[str, Any],
58
+ search_fields: list[ColumnElement[Any]] | None = None,
59
+ ) -> list[ColumnElement[Any]]:
60
+ """Parse Refine simple-rest filters from query parameters.
61
+
62
+ Supports json-server style operators:
63
+ - eq (default): field=value
64
+ - ne: field_ne=value
65
+ - gte: field_gte=value
66
+ - lte: field_lte=value
67
+ - like: field_like=value (contains match)
68
+
69
+ Also supports full-text search via q parameter.
70
+
71
+ Args:
72
+ query_params: FastAPI/Starlette query parameters
73
+ filter_fields: Mapping of field names to FilterField configs
74
+ search_fields: Columns to search for q parameter
75
+
76
+ Returns:
77
+ List of SQLAlchemy conditions
78
+ """
79
+ conditions: list[ColumnElement[Any]] = []
80
+
81
+ for key, value in query_params.multi_items():
82
+ if key in IGNORED_QUERY_KEYS:
83
+ continue
84
+
85
+ # Full-text search
86
+ if key == "q":
87
+ if search_fields:
88
+ pattern = f"%{value}%"
89
+ conditions.append(
90
+ or_(*(field.ilike(pattern) for field in search_fields))
91
+ )
92
+ continue
93
+
94
+ # Field filters
95
+ field, op = split_filter_key(key)
96
+ field_spec = filter_fields.get(field)
97
+ if not field_spec:
98
+ continue
99
+
100
+ try:
101
+ typed_value = field_spec.cast(value)
102
+ except (TypeError, ValueError):
103
+ continue
104
+
105
+ column = field_spec.column
106
+ if op == "eq":
107
+ conditions.append(column == typed_value)
108
+ elif op == "ne":
109
+ conditions.append(column != typed_value)
110
+ elif op == "gte":
111
+ conditions.append(column >= typed_value)
112
+ elif op == "lte":
113
+ conditions.append(column <= typed_value)
114
+ elif op == "like":
115
+ if isinstance(typed_value, str):
116
+ conditions.append(column.ilike(f"%{value}%"))
117
+ else:
118
+ conditions.append(column == typed_value)
119
+
120
+ return conditions
121
+
122
+
123
+ def parse_sorters(
124
+ _sort: str | None,
125
+ _order: str | None,
126
+ *,
127
+ sort_fields: dict[str, ColumnElement[Any]],
128
+ ) -> list[ColumnElement[Any]]:
129
+ """Parse Refine simple-rest sorters from query parameters.
130
+
131
+ Supports comma-separated fields and orders:
132
+ _sort=title,createdAt&_order=asc,desc
133
+
134
+ Args:
135
+ _sort: Comma-separated field names to sort by
136
+ _order: Comma-separated order directions (asc/desc)
137
+ sort_fields: Mapping of field names to SQLAlchemy columns
138
+
139
+ Returns:
140
+ List of SQLAlchemy order by clauses
141
+ """
142
+ if not _sort:
143
+ return []
144
+
145
+ sort_fields_list = [field.strip() for field in _sort.split(",") if field.strip()]
146
+ order_list = (
147
+ [order.strip().lower() for order in _order.split(",")] if _order else []
148
+ )
149
+
150
+ order_by: list[ColumnElement[Any]] = []
151
+ for index, field in enumerate(sort_fields_list):
152
+ column = sort_fields.get(field)
153
+ if not column:
154
+ continue
155
+
156
+ order = order_list[index] if index < len(order_list) else "asc"
157
+ order_by.append(column.desc() if order == "desc" else column.asc())
158
+
159
+ return order_by
160
+
161
+
162
+ def resolve_pagination(
163
+ *,
164
+ _start: int | None,
165
+ _end: int | None,
166
+ skip: int,
167
+ limit: int,
168
+ ) -> tuple[int, int]:
169
+ """Resolve pagination from Refine simple-rest parameters.
170
+
171
+ Supports both range-based (_start, _end) and offset-based (skip, limit) pagination.
172
+
173
+ Args:
174
+ _start: Range start (0-based, inclusive)
175
+ _end: Range end (0-based, exclusive)
176
+ skip: Offset for skip/limit pagination
177
+ limit: Maximum number of items to return
178
+
179
+ Returns:
180
+ Tuple of (offset, limit) for SQLAlchemy queries
181
+ """
182
+ if _start is None and _end is None:
183
+ return skip, limit
184
+
185
+ start = _start or 0
186
+ end = _end if _end is not None else start + limit
187
+ return start, max(0, end - start)
@@ -0,0 +1,63 @@
1
+ """Core type definitions for fastapi-refine."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from dataclasses import dataclass
7
+ from typing import Any
8
+
9
+ from sqlalchemy import ColumnElement
10
+
11
+ __all__ = ["FilterField", "FilterConfig", "SortConfig", "PaginationConfig"]
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class FilterField:
16
+ """Field filter configuration.
17
+
18
+ Args:
19
+ column: SQLAlchemy column reference
20
+ cast: Type converter function (str -> target type)
21
+ """
22
+
23
+ column: ColumnElement[Any]
24
+ cast: Callable[[str], Any]
25
+
26
+
27
+ @dataclass
28
+ class FilterConfig:
29
+ """Filter configuration.
30
+
31
+ Args:
32
+ fields: Mapping of field names to FilterField configs
33
+ search_fields: List of columns for full-text search (q parameter)
34
+ """
35
+
36
+ fields: dict[str, FilterField]
37
+ search_fields: list[ColumnElement[Any]] | None = None
38
+
39
+
40
+ @dataclass
41
+ class SortConfig:
42
+ """Sort configuration.
43
+
44
+ Args:
45
+ fields: Mapping of field names to SQLAlchemy columns
46
+ """
47
+
48
+ fields: dict[str, ColumnElement[Any]]
49
+
50
+
51
+ @dataclass
52
+ class PaginationConfig:
53
+ """Pagination configuration.
54
+
55
+ Args:
56
+ default_skip: Default offset for skip pagination
57
+ default_limit: Default limit for skip/limit pagination
58
+ max_limit: Maximum allowed limit (prevents excessive queries)
59
+ """
60
+
61
+ default_skip: int = 0
62
+ default_limit: int = 100
63
+ max_limit: int = 1000
@@ -0,0 +1,6 @@
1
+ """Dependency injection modules for fastapi-refine."""
2
+
3
+ from fastapi_refine.dependencies.query import RefineQuery, refine_query
4
+ from fastapi_refine.dependencies.response import RefineResponse, refine_response
5
+
6
+ __all__ = ["RefineQuery", "RefineResponse", "refine_query", "refine_response"]
@@ -0,0 +1,156 @@
1
+ """Dependency injection for Refine query parsing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from fastapi import Query, Request
8
+ from sqlalchemy import ColumnElement
9
+ from sqlmodel import SQLModel
10
+
11
+ from fastapi_refine.core import FilterConfig, PaginationConfig, SortConfig
12
+ from fastapi_refine.core.query import (
13
+ parse_filters,
14
+ parse_sorters,
15
+ resolve_pagination,
16
+ )
17
+
18
+ __all__ = ["RefineQuery", "refine_query"]
19
+
20
+
21
+ class RefineQuery:
22
+ """Refine query parameters parsed from request.
23
+
24
+ This class provides convenient access to parsed query parameters
25
+ following Refine simple-rest conventions.
26
+
27
+ Attributes:
28
+ model: The SQLModel class for this resource
29
+ conditions: List of SQLAlchemy filter conditions
30
+ order_by: List of SQLAlchemy order by clauses
31
+ offset: Query offset for pagination
32
+ limit: Query limit for pagination
33
+ """
34
+
35
+ def __init__(
36
+ self,
37
+ model: type[SQLModel],
38
+ filter_config: FilterConfig,
39
+ sort_config: SortConfig,
40
+ pagination_config: PaginationConfig | None = None,
41
+ *,
42
+ _start: int | None = None,
43
+ _end: int | None = None,
44
+ _sort: str | None = None,
45
+ _order: str | None = None,
46
+ skip: int = 0,
47
+ limit: int = 100,
48
+ request: Request | None = None,
49
+ ):
50
+ self.model = model
51
+ self.filter_config = filter_config
52
+ self.sort_config = sort_config
53
+ self.pagination_config = pagination_config or PaginationConfig()
54
+
55
+ # Parse filters
56
+ if request:
57
+ self.conditions = parse_filters(
58
+ request.query_params,
59
+ filter_fields=filter_config.fields,
60
+ search_fields=filter_config.search_fields,
61
+ )
62
+ else:
63
+ self.conditions = []
64
+
65
+ # Parse sorters
66
+ self.order_by = parse_sorters(_sort, _order, sort_fields=sort_config.fields)
67
+
68
+ # Parse pagination
69
+ self.offset, self.limit = resolve_pagination(
70
+ _start=_start,
71
+ _end=_end,
72
+ skip=skip,
73
+ limit=min(limit, self.pagination_config.max_limit),
74
+ )
75
+
76
+ def get_count(
77
+ self, session: Any, conditions: list[ColumnElement[Any]] | None = None
78
+ ) -> int:
79
+ """Get total count of records matching conditions.
80
+
81
+ Args:
82
+ session: SQLAlchemy/SQLModel session
83
+ conditions: Optional list of conditions (uses self.conditions if None)
84
+
85
+ Returns:
86
+ Total count of matching records
87
+ """
88
+ from sqlalchemy import func, select
89
+
90
+ conditions = conditions if conditions is not None else self.conditions
91
+
92
+ count_statement = select(func.count()).select_from(self.model)
93
+ if conditions:
94
+ count_statement = count_statement.where(*conditions)
95
+
96
+ return session.scalar(count_statement) or 0
97
+
98
+
99
+ def refine_query(
100
+ model: type[SQLModel],
101
+ filter_config: FilterConfig,
102
+ sort_config: SortConfig,
103
+ pagination_config: PaginationConfig | None = None,
104
+ ) -> type[RefineQuery]:
105
+ """Create a RefineQuery dependency.
106
+
107
+ Use this function with FastAPI's Depends:
108
+
109
+ ```python
110
+ query_dep = refine_query(Item, filter_config, sort_config)
111
+
112
+ @router.get("/")
113
+ def read_items(
114
+ query: RefineQuery = Depends(query_dep),
115
+ ...
116
+ ):
117
+ conditions = query.conditions
118
+ order_by = query.order_by
119
+ offset, limit = query.offset, query.limit
120
+ ...
121
+ ```
122
+
123
+ Args:
124
+ model: SQLModel class
125
+ filter_config: Filter configuration
126
+ sort_config: Sort configuration
127
+ pagination_config: Optional pagination configuration
128
+
129
+ Returns:
130
+ A callable that can be used with FastAPI's Depends
131
+ """
132
+
133
+ def dependency(
134
+ request: Request,
135
+ _start: int | None = Query(None, alias="_start"),
136
+ _end: int | None = Query(None, alias="_end"),
137
+ _sort: str | None = Query(None, alias="_sort"),
138
+ _order: str | None = Query(None, alias="_order"),
139
+ skip: int = 0,
140
+ limit: int = 100,
141
+ ) -> RefineQuery:
142
+ return RefineQuery(
143
+ model=model,
144
+ filter_config=filter_config,
145
+ sort_config=sort_config,
146
+ pagination_config=pagination_config,
147
+ _start=_start,
148
+ _end=_end,
149
+ _sort=_sort,
150
+ _order=_order,
151
+ skip=skip,
152
+ limit=limit,
153
+ request=request,
154
+ )
155
+
156
+ return dependency # type: ignore[return-value]
@@ -0,0 +1,51 @@
1
+ """Response handling for Refine simple-rest conventions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from fastapi import Response
6
+
7
+ __all__ = ["RefineResponse", "refine_response"]
8
+
9
+
10
+ class _RefineResponseHelper:
11
+ """Helper for setting Refine-specific response headers.
12
+
13
+ Refine's simple-rest data provider expects an x-total-count header
14
+ for list responses to support server-side pagination.
15
+ """
16
+
17
+ def __init__(self, response: Response):
18
+ self._response = response
19
+
20
+ def set_total_count(self, count: int) -> None:
21
+ """Set the x-total-count header.
22
+
23
+ Args:
24
+ count: Total number of records (for pagination)
25
+ """
26
+ self._response.headers["x-total-count"] = str(count)
27
+
28
+
29
+ # Type alias for backwards compatibility
30
+ RefineResponse = _RefineResponseHelper
31
+
32
+
33
+ def refine_response() -> _RefineResponseHelper:
34
+ """Dependency that creates a RefineResponse helper.
35
+
36
+ Usage:
37
+ ```python
38
+ @router.get("/")
39
+ def read_items(
40
+ response: RefineResponse = Depends(refine_response),
41
+ ...
42
+ ):
43
+ response.set_total_count(100)
44
+ return items
45
+ ```
46
+ """
47
+
48
+ def dependency(resp: Response) -> _RefineResponseHelper:
49
+ return _RefineResponseHelper(resp)
50
+
51
+ return dependency # type: ignore[return-value]
@@ -0,0 +1,6 @@
1
+ """Hook system for customizing CRUD behavior."""
2
+
3
+ from fastapi_refine.hooks.base import HookContext, RefineHooks
4
+ from fastapi_refine.hooks.builtin import OwnerBasedHooks
5
+
6
+ __all__ = ["HookContext", "RefineHooks", "OwnerBasedHooks"]
@@ -0,0 +1,78 @@
1
+ """Hook system for customizing CRUD behavior."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Awaitable, Callable
6
+ from dataclasses import dataclass
7
+ from typing import Any
8
+
9
+ from sqlalchemy import ColumnElement
10
+ from sqlmodel import SQLModel
11
+
12
+ __all__ = ["RefineHooks", "HookContext"]
13
+
14
+
15
+ # Hook type aliases
16
+ BeforeQueryHook = Callable[
17
+ ["HookContext", list[ColumnElement[Any]]],
18
+ list[ColumnElement[Any]] | Awaitable[list[ColumnElement[Any]]],
19
+ ]
20
+
21
+ AfterQueryHook = Callable[
22
+ ["HookContext", list[Any]],
23
+ list[Any] | Awaitable[list[Any]],
24
+ ]
25
+
26
+ BeforeMutationHook = Callable[
27
+ ["HookContext", Any],
28
+ None | Awaitable[None],
29
+ ]
30
+
31
+ AfterMutationHook = Callable[
32
+ ["HookContext", Any, Any],
33
+ Any | Awaitable[Any],
34
+ ]
35
+
36
+
37
+ @dataclass
38
+ class HookContext:
39
+ """Context passed to hooks during execution.
40
+
41
+ Attributes:
42
+ model: The SQLModel class being operated on
43
+ session: Database session
44
+ current_user: Currently authenticated user (if available)
45
+ request: Current FastAPI request (if available)
46
+ """
47
+
48
+ model: type[SQLModel]
49
+ session: Any
50
+ current_user: Any | None = None
51
+ request: Any | None = None
52
+
53
+
54
+ @dataclass
55
+ class RefineHooks:
56
+ """Collection of lifecycle hooks for CRUD operations.
57
+
58
+ All hooks are optional. Define only the ones you need.
59
+
60
+ Attributes:
61
+ before_query: Called before query execution, can modify conditions
62
+ after_query: Called after query execution, can modify results
63
+ before_create: Called before creating a record, can raise for permission check
64
+ after_create: Called after creating a record, can modify the result
65
+ before_update: Called before updating a record, can raise for permission check
66
+ after_update: Called after updating a record, can modify the result
67
+ before_delete: Called before deleting a record, can raise for permission check
68
+ after_delete: Called after deleting a record
69
+ """
70
+
71
+ before_query: BeforeQueryHook | None = None
72
+ after_query: AfterQueryHook | None = None
73
+ before_create: BeforeMutationHook | None = None
74
+ after_create: AfterMutationHook | None = None
75
+ before_update: BeforeMutationHook | None = None
76
+ after_update: AfterMutationHook | None = None
77
+ before_delete: BeforeMutationHook | None = None
78
+ after_delete: AfterMutationHook | None = None
@@ -0,0 +1,93 @@
1
+ """Built-in hook implementations for common use cases."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from fastapi import HTTPException, status
8
+ from sqlalchemy import ColumnElement
9
+
10
+ from fastapi_refine.hooks.base import HookContext, RefineHooks
11
+
12
+ __all__ = ["OwnerBasedHooks"]
13
+
14
+
15
+ class OwnerBasedHooks(RefineHooks):
16
+ """Hooks for owner-based permission control.
17
+
18
+ Ensures users can only access records they own, unless they are superusers.
19
+
20
+ Example:
21
+ ```python
22
+ hooks = OwnerBasedHooks(
23
+ owner_field="owner_id",
24
+ allow_superuser=True,
25
+ )
26
+ ```
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ owner_field: str = "owner_id",
32
+ allow_superuser: bool = True,
33
+ ):
34
+ """Initialize owner-based hooks.
35
+
36
+ Args:
37
+ owner_field: Name of the field containing the owner's user ID
38
+ allow_superuser: Whether to allow superusers to access all records
39
+ """
40
+ self.owner_field = owner_field
41
+ self.allow_superuser = allow_superuser
42
+ super().__init__(
43
+ before_query=self._before_query,
44
+ before_update=self._before_mutation,
45
+ before_delete=self._before_mutation,
46
+ )
47
+
48
+ def _before_query(
49
+ self,
50
+ context: HookContext,
51
+ conditions: list[ColumnElement[Any]],
52
+ ) -> list[ColumnElement[Any]]:
53
+ """Add owner filter to query conditions."""
54
+ if not context.current_user:
55
+ return conditions
56
+
57
+ if self.allow_superuser and getattr(
58
+ context.current_user, "is_superuser", False
59
+ ):
60
+ return conditions
61
+
62
+ user_id = getattr(context.current_user, "id", None)
63
+ if not user_id:
64
+ return conditions
65
+
66
+ # Add owner_id filter
67
+ model_class = context.model
68
+ owner_column = getattr(model_class, self.owner_field)
69
+ conditions.append(owner_column == user_id)
70
+
71
+ return conditions
72
+
73
+ def _before_mutation(self, context: HookContext, item: Any) -> None:
74
+ """Check if user has permission to modify/delete this item."""
75
+ if not context.current_user:
76
+ raise HTTPException(
77
+ status_code=status.HTTP_401_UNAUTHORIZED,
78
+ detail="Authentication required",
79
+ )
80
+
81
+ if self.allow_superuser and getattr(
82
+ context.current_user, "is_superuser", False
83
+ ):
84
+ return
85
+
86
+ user_id = getattr(context.current_user, "id", None)
87
+ owner_id = getattr(item, self.owner_field, None)
88
+
89
+ if owner_id != user_id:
90
+ raise HTTPException(
91
+ status_code=status.HTTP_403_FORBIDDEN,
92
+ detail="Not enough permissions",
93
+ )
File without changes
@@ -0,0 +1,5 @@
1
+ """Router factory for generating Refine-compatible CRUD endpoints."""
2
+
3
+ from fastapi_refine.routers.factory import RefineCRUDRouter
4
+
5
+ __all__ = ["RefineCRUDRouter"]
@@ -0,0 +1,331 @@
1
+ """CRUD Router factory for generating standard Refine-compatible endpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Generic, TypeVar
6
+
7
+ from fastapi import APIRouter, HTTPException, Query, Response, status
8
+ from sqlmodel import Session, SQLModel, select
9
+
10
+ from fastapi_refine.core import FilterConfig, PaginationConfig, SortConfig
11
+ from fastapi_refine.dependencies import RefineQuery, RefineResponse
12
+ from fastapi_refine.hooks import HookContext, RefineHooks
13
+
14
+ __all__ = ["RefineCRUDRouter"]
15
+
16
+ ModelT = TypeVar("ModelT", bound=SQLModel)
17
+ CreateSchemaT = TypeVar("CreateSchemaT", bound=SQLModel)
18
+ UpdateSchemaT = TypeVar("UpdateSchemaT", bound=SQLModel)
19
+ PublicSchemaT = TypeVar("PublicSchemaT", bound=SQLModel)
20
+
21
+
22
+ class RefineCRUDRouter(Generic[ModelT, CreateSchemaT, UpdateSchemaT, PublicSchemaT]):
23
+ """Factory for generating Refine-compatible CRUD routers.
24
+
25
+ Automatically creates standard CRUD endpoints that follow Refine simple-rest conventions:
26
+ - GET /{resource}/ - List with pagination, sorting, filtering
27
+ - GET /{resource}/{id} - Get single item
28
+ - POST /{resource}/ - Create new item
29
+ - PATCH /{resource}/{id} - Update item
30
+ - DELETE /{resource}/{id} - Delete item
31
+
32
+ Example:
33
+ ```python
34
+ router = RefineCRUDRouter(
35
+ model=Item,
36
+ prefix="/items",
37
+ create_schema=ItemCreate,
38
+ update_schema=ItemUpdate,
39
+ public_schema=ItemPublic,
40
+ session_dep=SessionDep,
41
+ filter_config=filter_config,
42
+ sort_config=sort_config,
43
+ current_user_dep=CurrentUser,
44
+ hooks=OwnerBasedHooks(owner_field="owner_id"),
45
+ ).router
46
+ ```
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ model: type[ModelT],
52
+ prefix: str,
53
+ create_schema: type[CreateSchemaT],
54
+ update_schema: type[UpdateSchemaT],
55
+ public_schema: type[PublicSchemaT],
56
+ session_dep: Any,
57
+ filter_config: FilterConfig,
58
+ sort_config: SortConfig,
59
+ pagination_config: PaginationConfig | None = None,
60
+ hooks: RefineHooks | None = None,
61
+ current_user_dep: Any | None = None,
62
+ tags: list[str] | None = None,
63
+ ):
64
+ """Initialize the CRUD router.
65
+
66
+ Args:
67
+ model: SQLModel database class
68
+ prefix: URL prefix for routes (e.g., "/items")
69
+ create_schema: Pydantic schema for creating items
70
+ update_schema: Pydantic schema for updating items
71
+ public_schema: Pydantic schema for API responses
72
+ session_dep: FastAPI dependency for database session
73
+ filter_config: Filter configuration
74
+ sort_config: Sort configuration
75
+ pagination_config: Optional pagination configuration
76
+ hooks: Optional lifecycle hooks
77
+ current_user_dep: Optional FastAPI dependency for current user
78
+ tags: OpenAPI tags for documentation
79
+ """
80
+ self.model = model
81
+ self.create_schema = create_schema
82
+ self.update_schema = update_schema
83
+ self.public_schema = public_schema
84
+ self.session_dep = session_dep
85
+ self.filter_config = filter_config
86
+ self.sort_config = sort_config
87
+ self.pagination_config = pagination_config or PaginationConfig()
88
+ self.hooks = hooks or RefineHooks()
89
+ self.current_user_dep = current_user_dep
90
+
91
+ self.router = APIRouter(prefix=prefix, tags=tags or [prefix.strip("/")]) # type: ignore[arg-type]
92
+ self._setup_routes()
93
+
94
+ def _setup_routes(self) -> None:
95
+ """Setup all CRUD routes."""
96
+ self.router.add_api_route(
97
+ "/",
98
+ self.get_list,
99
+ methods=["GET"],
100
+ response_model=list[self.public_schema], # type: ignore[name-defined]
101
+ )
102
+ self.router.add_api_route(
103
+ "/{id}",
104
+ self.get_one,
105
+ methods=["GET"],
106
+ response_model=self.public_schema,
107
+ )
108
+ self.router.add_api_route(
109
+ "/",
110
+ self.create,
111
+ methods=["POST"],
112
+ response_model=self.public_schema,
113
+ )
114
+ self.router.add_api_route(
115
+ "/{id}",
116
+ self.update,
117
+ methods=["PATCH"],
118
+ response_model=self.public_schema,
119
+ )
120
+ self.router.add_api_route(
121
+ "/{id}",
122
+ self.delete,
123
+ methods=["DELETE"],
124
+ )
125
+
126
+ def get_list(
127
+ self,
128
+ request: Any, # Request
129
+ response: Response,
130
+ session: Session,
131
+ skip: int = 0,
132
+ limit: int = 100,
133
+ _start: int | None = Query(None, alias="_start"),
134
+ _end: int | None = Query(None, alias="_end"),
135
+ _sort: str | None = Query(None, alias="_sort"),
136
+ _order: str | None = Query(None, alias="_order"),
137
+ id: list[Any] | None = Query(None),
138
+ ) -> list[Any]:
139
+ """Get list of items (Refine getList)."""
140
+ # Parse query
141
+ query = RefineQuery(
142
+ model=self.model,
143
+ filter_config=self.filter_config,
144
+ sort_config=self.sort_config,
145
+ pagination_config=self.pagination_config,
146
+ _start=_start,
147
+ _end=_end,
148
+ _sort=_sort,
149
+ _order=_order,
150
+ skip=skip,
151
+ limit=limit,
152
+ request=request,
153
+ )
154
+
155
+ conditions = query.conditions
156
+ if id:
157
+ conditions.append(self.model.id.in_(id)) # type: ignore[attr-defined]
158
+
159
+ # Execute before_query hook
160
+ if self.hooks.before_query:
161
+ current_user = self.current_user() if self.current_user_dep else None # type: ignore[attr-defined]
162
+ context = HookContext(
163
+ model=self.model,
164
+ session=session,
165
+ current_user=current_user,
166
+ request=request,
167
+ )
168
+ conditions = self._run_hook(self.hooks.before_query, context, conditions)
169
+
170
+ # Get count
171
+ count = query.get_count(session, conditions)
172
+ refine_response = RefineResponse(response)
173
+ refine_response.set_total_count(count)
174
+
175
+ # Execute query
176
+ statement = select(self.model)
177
+ if conditions:
178
+ statement = statement.where(*conditions)
179
+ if query.order_by:
180
+ statement = statement.order_by(*query.order_by)
181
+
182
+ items = list(
183
+ session.exec(statement.offset(query.offset).limit(query.limit)).all()
184
+ )
185
+
186
+ # Execute after_query hook
187
+ if self.hooks.after_query:
188
+ current_user = self.current_user() if self.current_user_dep else None # type: ignore[attr-defined]
189
+ context = HookContext(
190
+ model=self.model,
191
+ session=session,
192
+ current_user=current_user,
193
+ request=request,
194
+ )
195
+ items = self._run_hook(self.hooks.after_query, context, items)
196
+
197
+ return items
198
+
199
+ def get_one(self, id: Any, session: Session) -> Any:
200
+ """Get single item by ID (Refine getOne)."""
201
+ item = session.get(self.model, id)
202
+ if not item:
203
+ raise HTTPException(
204
+ status_code=status.HTTP_404_NOT_FOUND,
205
+ detail=f"{self.model.__name__} not found",
206
+ )
207
+ return item
208
+
209
+ def create(
210
+ self,
211
+ item_in: CreateSchemaT,
212
+ session: Session,
213
+ ) -> Any:
214
+ """Create new item (Refine create)."""
215
+ current_user = self.current_user_dep() if self.current_user_dep else None
216
+
217
+ # Execute before_create hook
218
+ if self.hooks.before_create:
219
+ context = HookContext(
220
+ model=self.model,
221
+ session=session,
222
+ current_user=current_user,
223
+ )
224
+ self._run_hook(self.hooks.before_create, context, item_in)
225
+
226
+ # Create item
227
+ item = self.model.model_validate(item_in)
228
+ session.add(item)
229
+ session.commit()
230
+ session.refresh(item)
231
+
232
+ # Execute after_create hook
233
+ if self.hooks.after_create:
234
+ context = HookContext(
235
+ model=self.model,
236
+ session=session,
237
+ current_user=current_user,
238
+ )
239
+ item = self._run_hook(self.hooks.after_create, context, item_in, item)
240
+
241
+ return item
242
+
243
+ def update(
244
+ self,
245
+ id: Any,
246
+ item_in: UpdateSchemaT,
247
+ session: Session,
248
+ ) -> Any:
249
+ """Update item (Refine update)."""
250
+ item = session.get(self.model, id)
251
+ if not item:
252
+ raise HTTPException(
253
+ status_code=status.HTTP_404_NOT_FOUND,
254
+ detail=f"{self.model.__name__} not found",
255
+ )
256
+
257
+ current_user = self.current_user_dep() if self.current_user_dep else None
258
+
259
+ # Execute before_update hook
260
+ if self.hooks.before_update:
261
+ context = HookContext(
262
+ model=self.model,
263
+ session=session,
264
+ current_user=current_user,
265
+ )
266
+ self._run_hook(self.hooks.before_update, context, item)
267
+
268
+ # Update item
269
+ update_data = item_in.model_dump(exclude_unset=True)
270
+ item.sqlmodel_update(update_data)
271
+ session.add(item)
272
+ session.commit()
273
+ session.refresh(item)
274
+
275
+ # Execute after_update hook
276
+ if self.hooks.after_update:
277
+ context = HookContext(
278
+ model=self.model,
279
+ session=session,
280
+ current_user=current_user,
281
+ )
282
+ item = self._run_hook(self.hooks.after_update, context, item, item)
283
+
284
+ return item
285
+
286
+ def delete(self, id: Any, session: Session) -> dict[str, str]:
287
+ """Delete item (Refine delete)."""
288
+ item = session.get(self.model, id)
289
+ if not item:
290
+ raise HTTPException(
291
+ status_code=status.HTTP_404_NOT_FOUND,
292
+ detail=f"{self.model.__name__} not found",
293
+ )
294
+
295
+ current_user = self.current_user_dep() if self.current_user_dep else None
296
+
297
+ # Execute before_delete hook
298
+ if self.hooks.before_delete:
299
+ context = HookContext(
300
+ model=self.model,
301
+ session=session,
302
+ current_user=current_user,
303
+ )
304
+ self._run_hook(self.hooks.before_delete, context, item)
305
+
306
+ # Delete item
307
+ session.delete(item)
308
+ session.commit()
309
+
310
+ # Execute after_delete hook
311
+ if self.hooks.after_delete:
312
+ context = HookContext(
313
+ model=self.model,
314
+ session=session,
315
+ current_user=current_user,
316
+ )
317
+ self._run_hook(self.hooks.after_delete, context, item)
318
+
319
+ return {"message": f"{self.model.__name__} deleted successfully"}
320
+
321
+ def _run_hook(self, hook: Any, *args: Any) -> Any:
322
+ """Run a hook, handling both sync and async hooks."""
323
+ import inspect
324
+
325
+ result = hook(*args)
326
+
327
+ if inspect.isawaitable(result):
328
+ # For now, we'll just return the awaitable as-is
329
+ # In a full async implementation, we'd await it here
330
+ return result
331
+ return result
File without changes
@@ -0,0 +1,282 @@
1
+ Metadata-Version: 2.4
2
+ Name: fastapi-refine
3
+ Version: 0.1.0
4
+ Summary: FastAPI integration for Refine simple-rest data provider
5
+ Project-URL: Homepage, https://github.com/koch3092/fastapi-refine
6
+ Project-URL: Documentation, https://github.com/koch3092/fastapi-refine#readme
7
+ Project-URL: Repository, https://github.com/koch3092/fastapi-refine
8
+ Project-URL: Bug Tracker, https://github.com/koch3092/fastapi-refine/issues
9
+ Author-email: koko <developer@dorakoch.com>
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: crud,fastapi,refine,rest,sqlalchemy,sqlmodel
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Framework :: FastAPI
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Internet :: WWW/HTTP
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.10
26
+ Requires-Dist: fastapi>=0.114.2
27
+ Requires-Dist: sqlmodel>=0.0.21
28
+ Provides-Extra: dev
29
+ Requires-Dist: mypy>=1.8.0; extra == 'dev'
30
+ Requires-Dist: pytest>=7.4.3; extra == 'dev'
31
+ Requires-Dist: ruff>=0.2.2; extra == 'dev'
32
+ Description-Content-Type: text/markdown
33
+
34
+ # fastapi-refine
35
+
36
+ [![PyPI version](https://badge.fury.io/py/fastapi-refine.svg)](https://badge.fury.io/py/fastapi-refine)
37
+ [![Python Versions](https://img.shields.io/pypi/pyversions/fastapi-refine.svg)](https://pypi.org/project/fastapi-refine/)
38
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
39
+
40
+ FastAPI integration for [Refine](https://refine.dev/) simple-rest data provider. Build type-safe, production-ready REST APIs that work seamlessly with Refine's data provider conventions.
41
+
42
+ ## Features
43
+
44
+ - **Automatic Query Parsing**: Parse Refine's filter, sort, and pagination parameters out-of-the-box
45
+ - **Type-Safe**: Full type hints and mypy strict mode compliance
46
+ - **SQLModel Integration**: First-class support for SQLModel/SQLAlchemy ORM
47
+ - **CRUD Router Factory**: Generate complete CRUD endpoints with one class
48
+ - **Flexible Filtering**: Support for `eq`, `ne`, `gte`, `lte`, `like` operators and full-text search
49
+ - **Hook System**: Inject custom logic before/after operations (permissions, validation, etc.)
50
+ - **Production Ready**: Built with FastAPI best practices
51
+
52
+ ## Installation
53
+
54
+ ```bash
55
+ pip install fastapi-refine
56
+ ```
57
+
58
+ ## Quick Start
59
+
60
+ ### Basic Usage with Manual Endpoints
61
+
62
+ ```python
63
+ from typing import Annotated
64
+
65
+ from fastapi import APIRouter, Depends
66
+ from sqlmodel import Session, select
67
+ from fastapi_refine import (
68
+ FilterConfig,
69
+ FilterField,
70
+ SortConfig,
71
+ RefineQuery,
72
+ RefineResponse,
73
+ refine_query,
74
+ refine_response,
75
+ )
76
+ from fastapi_refine.core import parse_bool
77
+
78
+ from .models import Item, ItemPublic
79
+ from .database import get_session
80
+
81
+ router = APIRouter(prefix="/items", tags=["items"])
82
+
83
+ SessionDep = Annotated[Session, Depends(get_session)]
84
+
85
+ # Configure which fields can be filtered and sorted
86
+ filter_config = FilterConfig(
87
+ fields={
88
+ "id": FilterField(Item.id, str),
89
+ "title": FilterField(Item.title, str),
90
+ "is_active": FilterField(Item.is_active, parse_bool),
91
+ },
92
+ search_fields=[Item.title, Item.description], # Full-text search fields
93
+ )
94
+
95
+ sort_config = SortConfig(
96
+ fields={
97
+ "id": Item.id,
98
+ "title": Item.title,
99
+ "created_at": Item.created_at,
100
+ }
101
+ )
102
+
103
+ @router.get("/", response_model=list[ItemPublic])
104
+ def read_items(
105
+ session: SessionDep,
106
+ refine_resp: Annotated[RefineResponse, Depends(refine_response())],
107
+ query: Annotated[RefineQuery, Depends(refine_query(Item, filter_config, sort_config))],
108
+ ) -> list[ItemPublic]:
109
+ # query.conditions contains parsed WHERE clauses
110
+ # query.order_by contains ORDER BY clauses
111
+ # query.offset and query.limit are ready for pagination
112
+
113
+ items = session.exec(
114
+ select(Item)
115
+ .where(*query.conditions)
116
+ .order_by(*query.order_by)
117
+ .offset(query.offset)
118
+ .limit(query.limit)
119
+ ).all()
120
+
121
+ # Set x-total-count header for Refine pagination
122
+ total = query.get_count(session, query.conditions)
123
+ refine_resp.set_total_count(total)
124
+
125
+ return items
126
+ ```
127
+
128
+ ### Automatic CRUD Router (Recommended)
129
+
130
+ Generate all CRUD endpoints automatically:
131
+
132
+ ```python
133
+ from fastapi import FastAPI
134
+ from fastapi_refine import RefineCRUDRouter, FilterConfig, FilterField, SortConfig
135
+ from fastapi_refine.core import parse_bool
136
+ from .models import Item, ItemCreate, ItemUpdate, ItemPublic
137
+ from .database import get_session
138
+
139
+ app = FastAPI()
140
+
141
+ # Create router with full CRUD operations
142
+ crud_router = RefineCRUDRouter(
143
+ model=Item,
144
+ prefix="/items",
145
+ create_schema=ItemCreate,
146
+ update_schema=ItemUpdate,
147
+ public_schema=ItemPublic,
148
+ session_dep=get_session,
149
+ filter_config=FilterConfig(
150
+ fields={
151
+ "title": FilterField(Item.title, str),
152
+ "is_active": FilterField(Item.is_active, parse_bool),
153
+ },
154
+ search_fields=[Item.title],
155
+ ),
156
+ sort_config=SortConfig(
157
+ fields={"title": Item.title, "created_at": Item.created_at}
158
+ ),
159
+ tags=["items"],
160
+ )
161
+
162
+ app.include_router(crud_router.router)
163
+ ```
164
+
165
+ This automatically creates:
166
+ - `GET /items/` - List with filtering, sorting, pagination
167
+ - `GET /items/{id}` - Get single item
168
+ - `POST /items/` - Create item
169
+ - `PATCH /items/{id}` - Update item
170
+ - `DELETE /items/{id}` - Delete item
171
+
172
+ ## Advanced Usage
173
+
174
+ ### Custom Hooks for Permissions
175
+
176
+ ```python
177
+ from fastapi import Depends, HTTPException
178
+ from fastapi_refine import RefineHooks, HookContext, RefineCRUDRouter
179
+
180
+ def before_query(context: HookContext, conditions: list) -> list:
181
+ """Filter items to only show user's own items"""
182
+ if context.current_user:
183
+ conditions.append(context.model.owner_id == context.current_user.id)
184
+ return conditions
185
+
186
+ def before_delete(context: HookContext, item) -> None:
187
+ """Only allow deleting own items"""
188
+ if item.owner_id != context.current_user.id:
189
+ raise HTTPException(status_code=403, detail="Not authorized")
190
+
191
+ hooks = RefineHooks(
192
+ before_query=before_query,
193
+ before_delete=before_delete,
194
+ )
195
+
196
+ crud_router = RefineCRUDRouter(
197
+ model=Item,
198
+ hooks=hooks,
199
+ current_user_dep=get_current_user,
200
+ # ... other config
201
+ )
202
+ ```
203
+
204
+ ### Pagination Configuration
205
+
206
+ ```python
207
+ from fastapi_refine import PaginationConfig, RefineCRUDRouter
208
+
209
+ pagination_config = PaginationConfig(
210
+ default_skip=0,
211
+ default_limit=50,
212
+ max_limit=500, # Prevent excessive queries
213
+ )
214
+
215
+ crud_router = RefineCRUDRouter(
216
+ pagination_config=pagination_config,
217
+ # ... other config
218
+ )
219
+ ```
220
+
221
+ ## Supported Query Parameters
222
+
223
+ The library parses Refine simple-rest query parameters:
224
+
225
+ ### Filtering
226
+ - `field=value` - Exact match (eq)
227
+ - `field_ne=value` - Not equal
228
+ - `field_gte=value` - Greater than or equal
229
+ - `field_lte=value` - Less than or equal
230
+ - `field_like=value` - Contains (case-insensitive)
231
+ - `q=search` - Full-text search across configured fields
232
+
233
+ ### Sorting
234
+ - `_sort=field1,field2` - Sort by multiple fields
235
+ - `_order=asc,desc` - Sort order for each field
236
+
237
+ ### Pagination
238
+ - Range-based: `_start=0&_end=20`
239
+ - Offset-based: `skip=0&limit=20`
240
+
241
+ ### Example Query
242
+ ```
243
+ GET /items?title_like=hello&is_active=true&_sort=created_at&_order=desc&_start=0&_end=10
244
+ ```
245
+
246
+ ## Type Converters
247
+
248
+ Built-in converters for common types:
249
+
250
+ ```python
251
+ from fastapi_refine import FilterConfig, FilterField
252
+ from fastapi_refine.core import parse_bool, parse_uuid
253
+
254
+ filter_config = FilterConfig(
255
+ fields={
256
+ "id": FilterField(Item.id, parse_uuid),
257
+ "is_active": FilterField(Item.is_active, parse_bool),
258
+ "price": FilterField(Item.price, float),
259
+ "quantity": FilterField(Item.quantity, int),
260
+ }
261
+ )
262
+ ```
263
+
264
+ ## Requirements
265
+
266
+ - Python 3.10+
267
+ - FastAPI 0.114.2+
268
+ - SQLModel 0.0.21+
269
+
270
+ ## Contributing
271
+
272
+ Contributions are welcome! Please feel free to submit a Pull Request.
273
+
274
+ ## License
275
+
276
+ MIT License - see LICENSE file for details.
277
+
278
+ ## Links
279
+
280
+ - [Refine Documentation](https://refine.dev/docs/)
281
+ - [FastAPI Documentation](https://fastapi.tiangolo.com/)
282
+ - [SQLModel Documentation](https://sqlmodel.tiangolo.com/)
@@ -0,0 +1,18 @@
1
+ fastapi_refine/__init__.py,sha256=jB3ePqiyivSvboP6Oiwyu-Y0DJzfTe_Pkz48pnFwBfI,643
2
+ fastapi_refine/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ fastapi_refine/core/__init__.py,sha256=4WeRIBcgUjNDjtO6UCiA5pD6wu1o-X1RokfOqahlrw0,491
4
+ fastapi_refine/core/query.py,sha256=x0poTtwpiT06a2aHqHLjM3BPpgK_QNafb4HxdEWG6gA,5242
5
+ fastapi_refine/core/types.py,sha256=hO_x5rRIs5FQt3LaQcvWLezyzNTXHOMDFGUxEzEIaEY,1412
6
+ fastapi_refine/dependencies/__init__.py,sha256=G0Ls3DmmqmOW-gcruNdskP1uZHsj_8c-aXQrazA1Zqk,289
7
+ fastapi_refine/dependencies/query.py,sha256=mAGCICC2sxMXwX7vtzRT05nrPq4tenEoWGRZ2PHd7Qs,4528
8
+ fastapi_refine/dependencies/response.py,sha256=1r_DgSSHK8FqKe0vB8OTUqST1m8k3YV35YTkRZzQbY0,1328
9
+ fastapi_refine/hooks/__init__.py,sha256=NMw5nZ2rTZeantO51rbIHhHBcmo3zuhyRnSoic1BtV4,231
10
+ fastapi_refine/hooks/base.py,sha256=mv_OaRIUYSq-wu5vcNeayb-51yGomRQSowayrEdTGf0,2358
11
+ fastapi_refine/hooks/builtin.py,sha256=NsNnITbg_qQN9-vuLpcvXPCLY9yFrb3rGZ-uuhRMG30,2733
12
+ fastapi_refine/routers/__init__.py,sha256=N4bnZXJjpJRpP-sS-aBXV-EwOBod3rStoMhycZ_HvLg,163
13
+ fastapi_refine/routers/factory.py,sha256=RUiP_MkZbv6vo8q3xDUlpH_9eNq55Nsplfu60kCFMsU,11182
14
+ fastapi_refine/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ fastapi_refine-0.1.0.dist-info/METADATA,sha256=60CpkMAOG1qgo1wULyHbvSYyum1ha15JmcKZBfgrl8g,8320
16
+ fastapi_refine-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
17
+ fastapi_refine-0.1.0.dist-info/licenses/LICENSE,sha256=fmW7UZeYMMP-iK31FXjWS3ZXQMpIlAYIdtohot23Fqo,1061
18
+ fastapi_refine-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 koko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.