python-flashapi 0.1.2__py3-none-any.whl → 0.2.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.
Files changed (48) hide show
  1. flashapi/__init__.py +8 -7
  2. flashapi/adapters/base.py +13 -12
  3. flashapi/adapters/django.py +771 -165
  4. flashapi/adapters/fastapi.py +879 -293
  5. flashapi/adapters/flask.py +728 -232
  6. flashapi/core/__init__.py +11 -3
  7. flashapi/core/custom_routes.py +5 -5
  8. flashapi/core/pluralize.py +80 -80
  9. flashapi/core/relations.py +59 -61
  10. flashapi/core/response.py +36 -33
  11. flashapi/core/schema.py +145 -83
  12. flashapi/core/visibility.py +46 -0
  13. flashapi/django.py +5 -5
  14. flashapi/docs/openapi.py +298 -223
  15. flashapi/fastapi.py +5 -5
  16. flashapi/features/__init__.py +6 -6
  17. flashapi/features/audit.py +84 -0
  18. flashapi/features/auth.py +134 -0
  19. flashapi/features/dashboard.py +412 -0
  20. flashapi/features/export.py +143 -0
  21. flashapi/features/filtering.py +124 -33
  22. flashapi/features/pagination.py +20 -20
  23. flashapi/features/rate_limit.py +45 -0
  24. flashapi/features/search.py +25 -25
  25. flashapi/features/sorting.py +23 -21
  26. flashapi/features/webhooks.py +84 -0
  27. flashapi/features/websocket.py +129 -0
  28. flashapi/flask.py +5 -5
  29. flashapi/inspectors/__init__.py +3 -3
  30. flashapi/inspectors/base.py +13 -11
  31. flashapi/inspectors/dataclass.py +50 -39
  32. flashapi/inspectors/detect.py +54 -48
  33. flashapi/inspectors/django.py +93 -83
  34. flashapi/inspectors/pydantic.py +99 -84
  35. flashapi/inspectors/sqlalchemy.py +83 -75
  36. flashapi/storage/__init__.py +4 -4
  37. flashapi/storage/auto.py +189 -106
  38. flashapi/storage/base.py +32 -26
  39. flashapi/storage/orm.py +125 -85
  40. flashapi/storage/sqlalchemy.py +56 -13
  41. python_flashapi-0.2.0.dist-info/METADATA +314 -0
  42. python_flashapi-0.2.0.dist-info/RECORD +48 -0
  43. python_flashapi-0.2.0.dist-info/licenses/LICENSE +190 -0
  44. python_flashapi-0.2.0.dist-info/licenses/NOTICE +5 -0
  45. python_flashapi-0.1.2.dist-info/METADATA +0 -259
  46. python_flashapi-0.1.2.dist-info/RECORD +0 -39
  47. python_flashapi-0.1.2.dist-info/licenses/LICENSE +0 -21
  48. {python_flashapi-0.1.2.dist-info → python_flashapi-0.2.0.dist-info}/WHEEL +0 -0
flashapi/core/__init__.py CHANGED
@@ -1,3 +1,11 @@
1
- from flashapi.core.schema import Model, ModelSchema, FieldSchema, FieldType, RelationSchema
2
-
3
- __all__ = ["Model", "ModelSchema", "FieldSchema", "FieldType", "RelationSchema"]
1
+ from flashapi.core.schema import (
2
+ FieldSchema,
3
+ FieldType,
4
+ FlashAPIConfigError,
5
+ Model,
6
+ ModelSchema,
7
+ RelationSchema,
8
+ validate_soft_delete,
9
+ )
10
+
11
+ __all__ = ["FieldSchema", "FieldType", "FlashAPIConfigError", "Model", "ModelSchema", "RelationSchema", "validate_soft_delete"]
@@ -14,7 +14,6 @@ from __future__ import annotations
14
14
  from dataclasses import dataclass, field
15
15
  from typing import Any
16
16
 
17
-
18
17
  TYPE_MAP = {
19
18
  "string": {"type": "string"},
20
19
  "str": {"type": "string"},
@@ -51,6 +50,7 @@ def api_doc(
51
50
  body_required: List of required field names in body.
52
51
  params: Query parameters as {name: type}.
53
52
  response: Response schema (raw OpenAPI schema dict).
53
+
54
54
  """
55
55
  def decorator(func):
56
56
  func._flashapi_doc = {
@@ -83,13 +83,13 @@ def _build_openapi_operation(doc: dict, method: str) -> dict[str, Any]:
83
83
  "tags": [doc["tag"]],
84
84
  "summary": doc["summary"] or f"{method.upper()} endpoint",
85
85
  "responses": {
86
- "200": {"description": "Success"}
86
+ "200": {"description": "Success"},
87
87
  },
88
88
  }
89
89
 
90
90
  if doc.get("response"):
91
91
  operation["responses"]["200"]["content"] = {
92
- "application/json": {"schema": doc["response"]}
92
+ "application/json": {"schema": doc["response"]},
93
93
  }
94
94
 
95
95
  if doc.get("params"):
@@ -224,13 +224,13 @@ def custom_routes_to_openapi_paths(routes: list[CustomRoute], trailing_slash: bo
224
224
  "tags": [route.tag],
225
225
  "summary": route.summary or f"{route.method.upper()} {route.path}",
226
226
  "responses": {
227
- "200": {"description": route.response_description}
227
+ "200": {"description": route.response_description},
228
228
  },
229
229
  }
230
230
 
231
231
  if route.response_schema:
232
232
  operation["responses"]["200"]["content"] = {
233
- "application/json": {"schema": route.response_schema}
233
+ "application/json": {"schema": route.response_schema},
234
234
  }
235
235
 
236
236
  if route.parameters:
@@ -1,80 +1,80 @@
1
- """Pluralization rules supporting English and French model names.
2
-
3
- Strategy:
4
- - Irregulars dict handles all known exceptions for both languages.
5
- - Rules are ordered to avoid cross-language conflicts.
6
- - For genuinely ambiguous cases, users can override via Model(plural=...).
7
- """
8
-
9
- IRREGULARS = {
10
- # English irregulars
11
- "person": "people",
12
- "child": "children",
13
- "mouse": "mice",
14
- "goose": "geese",
15
- "man": "men",
16
- "woman": "women",
17
- "tooth": "teeth",
18
- "foot": "feet",
19
- "datum": "data",
20
- "index": "indices",
21
- "leaf": "leaves",
22
- "knife": "knives",
23
- "wife": "wives",
24
- "life": "lives",
25
- "shelf": "shelves",
26
- "self": "selves",
27
- "half": "halves",
28
- "wolf": "wolves",
29
- # English words ending in -s/-x/-z that take -es (not invariable)
30
- "bus": "buses",
31
- "box": "boxes",
32
- "fox": "foxes",
33
- "buzz": "buzzes",
34
- "quiz": "quizzes",
35
- "fez": "fezzes",
36
- # French irregulars
37
- "travail": "travaux",
38
- "journal": "journaux",
39
- "oeil": "yeux",
40
- "monsieur": "messieurs",
41
- "madame": "mesdames",
42
- }
43
-
44
-
45
- def pluralize(word: str) -> str:
46
- lower = word.lower()
47
-
48
- if lower in IRREGULARS:
49
- return IRREGULARS[lower]
50
-
51
- # --- English: -ss, -sh, -ch → +es (must check before -s invariable rule) ---
52
- # en: class→classes, dish→dishes, match→matches
53
- if lower.endswith(("ss", "sh", "ch")):
54
- return lower + "es"
55
-
56
- # --- Invariable endings (both languages) ---
57
- # -s, -x, -z → no change (fr: temps, voix, nez / en: species)
58
- if lower.endswith(("s", "x", "z")):
59
- return lower
60
-
61
- # --- French rules ---
62
-
63
- # -eau, -au, -eu → +x (fr: niveau→niveaux, jeu→jeux, noyau→noyaux)
64
- if lower.endswith(("eau", "au", "eu")):
65
- return lower + "x"
66
-
67
- # -al → -aux (fr: animal→animaux, journal→journaux)
68
- # English exceptions (festival, carnival) are rare model names;
69
- # if needed, add them to IRREGULARS or use Model(plural=...)
70
- if lower.endswith("al"):
71
- return lower[:-2] + "aux"
72
-
73
- # consonant + y → -ies (en: category→categories, city→cities)
74
- if lower.endswith("y") and len(lower) >= 2 and lower[-2] not in "aeiou":
75
- return lower[:-1] + "ies"
76
-
77
- # --- Default: +s (works for both languages) ---
78
- # fr: maison→maisons, eleve→eleves, enseignant→enseignants
79
- # en: book→books, user→users, article→articles
80
- return lower + "s"
1
+ """Pluralization rules supporting English and French model names.
2
+
3
+ Strategy:
4
+ - Irregulars dict handles all known exceptions for both languages.
5
+ - Rules are ordered to avoid cross-language conflicts.
6
+ - For genuinely ambiguous cases, users can override via Model(plural=...).
7
+ """
8
+
9
+ IRREGULARS = {
10
+ # English irregulars
11
+ "person": "people",
12
+ "child": "children",
13
+ "mouse": "mice",
14
+ "goose": "geese",
15
+ "man": "men",
16
+ "woman": "women",
17
+ "tooth": "teeth",
18
+ "foot": "feet",
19
+ "datum": "data",
20
+ "index": "indices",
21
+ "leaf": "leaves",
22
+ "knife": "knives",
23
+ "wife": "wives",
24
+ "life": "lives",
25
+ "shelf": "shelves",
26
+ "self": "selves",
27
+ "half": "halves",
28
+ "wolf": "wolves",
29
+ # English words ending in -s/-x/-z that take -es (not invariable)
30
+ "bus": "buses",
31
+ "box": "boxes",
32
+ "fox": "foxes",
33
+ "buzz": "buzzes",
34
+ "quiz": "quizzes",
35
+ "fez": "fezzes",
36
+ # French irregulars
37
+ "travail": "travaux",
38
+ "journal": "journaux",
39
+ "oeil": "yeux",
40
+ "monsieur": "messieurs",
41
+ "madame": "mesdames",
42
+ }
43
+
44
+
45
+ def pluralize(word: str) -> str:
46
+ lower = word.lower()
47
+
48
+ if lower in IRREGULARS:
49
+ return IRREGULARS[lower]
50
+
51
+ # --- English: -ss, -sh, -ch → +es (must check before -s invariable rule) ---
52
+ # en: class→classes, dish→dishes, match→matches
53
+ if lower.endswith(("ss", "sh", "ch")):
54
+ return lower + "es"
55
+
56
+ # --- Invariable endings (both languages) ---
57
+ # -s, -x, -z → no change (fr: temps, voix, nez / en: species)
58
+ if lower.endswith(("s", "x", "z")):
59
+ return lower
60
+
61
+ # --- French rules ---
62
+
63
+ # -eau, -au, -eu → +x (fr: niveau→niveaux, jeu→jeux, noyau→noyaux)
64
+ if lower.endswith(("eau", "au", "eu")):
65
+ return lower + "x"
66
+
67
+ # -al → -aux (fr: animal→animaux, journal→journaux)
68
+ # English exceptions (festival, carnival) are rare model names;
69
+ # if needed, add them to IRREGULARS or use Model(plural=...)
70
+ if lower.endswith("al"):
71
+ return lower[:-2] + "aux"
72
+
73
+ # consonant + y → -ies (en: category→categories, city→cities)
74
+ if lower.endswith("y") and len(lower) >= 2 and lower[-2] not in "aeiou":
75
+ return lower[:-1] + "ies"
76
+
77
+ # --- Default: +s (works for both languages) ---
78
+ # fr: maison→maisons, eleve→eleves, enseignant→enseignants
79
+ # en: book→books, user→users, article→articles
80
+ return lower + "s"
@@ -1,61 +1,59 @@
1
- """Relation resolution between registered models."""
2
-
3
- from __future__ import annotations
4
-
5
- from flashapi.core.schema import ModelSchema, RelationSchema
6
-
7
-
8
- def resolve_relations(schemas: list[ModelSchema]) -> dict[str, list[RelationSchema]]:
9
- """
10
- Detect relations between models based on field names ending with _id.
11
- Returns a mapping: parent_plural -> list of child relations.
12
-
13
- Example: Book has author_id → Author is parent, Book is child.
14
- So "authors" -> [RelationSchema(type="one_to_many", target="books")]
15
- """
16
- schema_map = {s.name.lower(): s for s in schemas}
17
-
18
- parent_to_children: dict[str, list[RelationSchema]] = {}
19
-
20
- for schema in schemas:
21
- for field in schema.fields:
22
- if field.name.endswith("_id") and not field.primary_key:
23
- ref_name = field.name[:-3] # "author_id" → "author"
24
-
25
- target_schema = schema_map.get(ref_name)
26
- if target_schema is None:
27
- continue
28
-
29
- relation = RelationSchema(
30
- type="one_to_many",
31
- target=schema.plural,
32
- target_plural=schema.plural,
33
- foreign_key=field.name,
34
- )
35
-
36
- parent_plural = target_schema.plural
37
- if parent_plural not in parent_to_children:
38
- parent_to_children[parent_plural] = []
39
- parent_to_children[parent_plural].append(relation)
40
-
41
- field.relation = RelationSchema(
42
- type="many_to_one",
43
- target=target_schema.name,
44
- target_plural=target_schema.plural,
45
- foreign_key=field.name,
46
- )
47
-
48
- return parent_to_children
49
-
50
-
51
- def find_expandable_fields(schema: ModelSchema) -> dict[str, str]:
52
- """
53
- Return a mapping of expandable field names to their target plural.
54
- Example: {"author": "authors"} for a Book model with author_id.
55
- """
56
- expandable = {}
57
- for field in schema.fields:
58
- if field.relation and field.relation.type == "many_to_one":
59
- ref_name = field.name[:-3] # "author_id" → "author"
60
- expandable[ref_name] = field.relation.target_plural
61
- return expandable
1
+ """Relation resolution between registered models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from flashapi.core.schema import ModelSchema, RelationSchema
6
+
7
+
8
+ def resolve_relations(schemas: list[ModelSchema]) -> dict[str, list[RelationSchema]]:
9
+ """Detect relations between models based on field names ending with _id.
10
+ Returns a mapping: parent_plural -> list of child relations.
11
+
12
+ Example: Book has author_id → Author is parent, Book is child.
13
+ So "authors" -> [RelationSchema(type="one_to_many", target="books")]
14
+ """
15
+ schema_map = {s.name.lower(): s for s in schemas}
16
+
17
+ parent_to_children: dict[str, list[RelationSchema]] = {}
18
+
19
+ for schema in schemas:
20
+ for field in schema.fields:
21
+ if field.name.endswith("_id") and not field.primary_key:
22
+ ref_name = field.name[:-3] # "author_id" "author"
23
+
24
+ target_schema = schema_map.get(ref_name)
25
+ if target_schema is None:
26
+ continue
27
+
28
+ relation = RelationSchema(
29
+ type="one_to_many",
30
+ target=schema.plural,
31
+ target_plural=schema.plural,
32
+ foreign_key=field.name,
33
+ )
34
+
35
+ parent_plural = target_schema.plural
36
+ if parent_plural not in parent_to_children:
37
+ parent_to_children[parent_plural] = []
38
+ parent_to_children[parent_plural].append(relation)
39
+
40
+ field.relation = RelationSchema(
41
+ type="many_to_one",
42
+ target=target_schema.name,
43
+ target_plural=target_schema.plural,
44
+ foreign_key=field.name,
45
+ )
46
+
47
+ return parent_to_children
48
+
49
+
50
+ def find_expandable_fields(schema: ModelSchema) -> dict[str, str]:
51
+ """Return a mapping of expandable field names to their target plural.
52
+ Example: {"author": "authors"} for a Book model with author_id.
53
+ """
54
+ expandable = {}
55
+ for field in schema.fields:
56
+ if field.relation and field.relation.type == "many_to_one":
57
+ ref_name = field.name[:-3] # "author_id" → "author"
58
+ expandable[ref_name] = field.relation.target_plural
59
+ return expandable
flashapi/core/response.py CHANGED
@@ -1,33 +1,36 @@
1
- """Response formatting."""
2
-
3
- from typing import Callable
4
-
5
-
6
- def default_formatter(data: list | dict, meta: dict | None = None) -> dict:
7
- if isinstance(data, list):
8
- response = {"data": data}
9
- if meta:
10
- response.update(meta)
11
- return response
12
- return {"data": data}
13
-
14
-
15
- def create_list_response(
16
- data: list[dict],
17
- total: int,
18
- page: int,
19
- page_size: int,
20
- formatter: Callable | None = None,
21
- ) -> dict:
22
- pages = (total + page_size - 1) // page_size
23
- meta = {"total": total, "page": page, "pages": pages, "page_size": page_size}
24
-
25
- if formatter:
26
- return formatter(data, meta)
27
- return default_formatter(data, meta)
28
-
29
-
30
- def create_item_response(data: dict, formatter: Callable | None = None) -> dict:
31
- if formatter:
32
- return formatter(data, None)
33
- return default_formatter(data)
1
+ """Response formatting."""
2
+
3
+ from collections.abc import Callable
4
+
5
+
6
+ def create_list_response(
7
+ data: list[dict],
8
+ total: int,
9
+ page: int,
10
+ size: int,
11
+ formatter: Callable | None = None,
12
+ ) -> dict:
13
+ total_pages = (total + size - 1) // size if size > 0 else 0
14
+ response = {
15
+ "data": data,
16
+ "meta": {
17
+ "page": page,
18
+ "size": size,
19
+ "totalElements": total,
20
+ "totalPages": total_pages,
21
+ },
22
+ }
23
+ if formatter:
24
+ return formatter(response)
25
+ return response
26
+
27
+
28
+ def create_item_response(data: dict, formatter: Callable | None = None) -> dict:
29
+ response = {"data": data}
30
+ if formatter:
31
+ return formatter(response)
32
+ return response
33
+
34
+
35
+ def create_error_response(message: str, status: int) -> dict:
36
+ return {"error": message, "status": status}
flashapi/core/schema.py CHANGED
@@ -1,83 +1,145 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass, field
4
- from enum import Enum
5
- from typing import Any
6
-
7
-
8
- class FieldType(Enum):
9
- STRING = "string"
10
- INTEGER = "integer"
11
- FLOAT = "float"
12
- BOOLEAN = "boolean"
13
- DATE = "date"
14
- DATETIME = "datetime"
15
- TIME = "time"
16
- UUID = "uuid"
17
- JSON = "json"
18
- TEXT = "text"
19
- BINARY = "binary"
20
-
21
-
22
- @dataclass
23
- class RelationSchema:
24
- type: str # "one_to_one", "many_to_one", "one_to_many", "many_to_many"
25
- target: str
26
- target_plural: str = ""
27
- foreign_key: str = ""
28
-
29
-
30
- @dataclass
31
- class FieldSchema:
32
- name: str
33
- type: FieldType
34
- required: bool = True
35
- default: Any = None
36
- constraints: dict = field(default_factory=dict)
37
- primary_key: bool = False
38
- auto_generated: bool = False
39
- relation: RelationSchema | None = None
40
-
41
-
42
- @dataclass
43
- class ModelSchema:
44
- name: str
45
- plural: str
46
- fields: list[FieldSchema]
47
- permissions: list[str] = field(
48
- default_factory=lambda: ["list", "read", "create", "update", "delete"]
49
- )
50
-
51
-
52
- ALL_OPERATIONS = ["list", "read", "create", "update", "delete"]
53
-
54
-
55
- class Model:
56
- """Wrapper to configure how a model is exposed via FlashAPI."""
57
-
58
- def __init__(
59
- self,
60
- model_class: type,
61
- *,
62
- readonly: bool = False,
63
- exclude: list[str] | None = None,
64
- only: list[str] | None = None,
65
- plural: str | None = None,
66
- ):
67
- self.model_class = model_class
68
- self.plural = plural
69
- self.permissions = self._resolve_permissions(readonly, exclude, only)
70
-
71
- def _resolve_permissions(
72
- self,
73
- readonly: bool,
74
- exclude: list[str] | None,
75
- only: list[str] | None,
76
- ) -> list[str]:
77
- if only:
78
- return [op for op in only if op in ALL_OPERATIONS]
79
- if readonly:
80
- return ["list", "read"]
81
- if exclude:
82
- return [op for op in ALL_OPERATIONS if op not in exclude]
83
- return list(ALL_OPERATIONS)
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from enum import Enum
5
+ from typing import Any
6
+
7
+
8
+ class FieldType(Enum):
9
+ STRING = "string"
10
+ INTEGER = "integer"
11
+ FLOAT = "float"
12
+ BOOLEAN = "boolean"
13
+ DATE = "date"
14
+ DATETIME = "datetime"
15
+ TIME = "time"
16
+ UUID = "uuid"
17
+ JSON = "json"
18
+ TEXT = "text"
19
+ BINARY = "binary"
20
+
21
+
22
+ @dataclass
23
+ class RelationSchema:
24
+ type: str # "one_to_one", "many_to_one", "one_to_many", "many_to_many"
25
+ target: str
26
+ target_plural: str = ""
27
+ foreign_key: str = ""
28
+
29
+
30
+ @dataclass
31
+ class FieldSchema:
32
+ name: str
33
+ type: FieldType
34
+ required: bool = True
35
+ default: Any = None
36
+ constraints: dict = field(default_factory=dict)
37
+ primary_key: bool = False
38
+ auto_generated: bool = False
39
+ relation: RelationSchema | None = None
40
+ readonly: bool = False
41
+ writeonly: bool = False
42
+ hidden: bool = False
43
+ export_exclude: bool = False
44
+ auto: str | None = None # "uuid", "datetime", "date" — server-generated on create
45
+
46
+
47
+ @dataclass
48
+ class ModelSchema:
49
+ name: str
50
+ plural: str
51
+ fields: list[FieldSchema]
52
+ permissions: list[str] = field(
53
+ default_factory=lambda: ["list", "read", "create", "update", "delete"],
54
+ )
55
+ soft_delete: bool = False
56
+ audit: bool = False
57
+ lookup_field: str = "id"
58
+ access: str | dict | bool | None = None
59
+ scope: str | None = None # "tenant", "owner", or "both"
60
+ tenant_field: str | None = None
61
+ owner_field: str | None = None
62
+
63
+
64
+ ALL_OPERATIONS = ["list", "read", "create", "update", "delete"]
65
+
66
+ SOFT_DELETE_FIELD = "deleted_at"
67
+
68
+
69
+ class FlashAPIConfigError(Exception):
70
+ pass
71
+
72
+
73
+ def validate_soft_delete(model_class: type, soft_delete: bool) -> None:
74
+ """Raise if soft_delete=True but the model has no deleted_at field."""
75
+ if not soft_delete:
76
+ return
77
+
78
+ if hasattr(model_class, "_meta"):
79
+ try:
80
+ model_class._meta.get_field(SOFT_DELETE_FIELD)
81
+ except Exception:
82
+ msg = (
83
+ f'Model "{model_class.__name__}" has soft_delete=True but no '
84
+ f"'{SOFT_DELETE_FIELD}' field. Add:\n"
85
+ f" {SOFT_DELETE_FIELD} = models.DateTimeField(null=True, blank=True)"
86
+ )
87
+ raise FlashAPIConfigError(
88
+ msg,
89
+ )
90
+ elif hasattr(model_class, "__table__"):
91
+ columns = {col.name for col in model_class.__table__.columns}
92
+ if SOFT_DELETE_FIELD not in columns:
93
+ msg = (
94
+ f'Model "{model_class.__name__}" has soft_delete=True but no '
95
+ f"'{SOFT_DELETE_FIELD}' column. Add:\n"
96
+ f" {SOFT_DELETE_FIELD} = Column(DateTime, nullable=True)"
97
+ )
98
+ raise FlashAPIConfigError(
99
+ msg,
100
+ )
101
+
102
+
103
+ class Model:
104
+ """Wrapper to configure how a model is exposed via FlashAPI."""
105
+
106
+ def __init__(
107
+ self,
108
+ model_class: type,
109
+ *,
110
+ readonly: bool = False,
111
+ exclude: list[str] | None = None,
112
+ only: list[str] | None = None,
113
+ plural: str | None = None,
114
+ soft_delete: bool = False,
115
+ audit: bool = False,
116
+ lookup_field: str = "id",
117
+ access: str | dict | bool | None = None,
118
+ scope: str | None = None,
119
+ tenant_field: str | None = None,
120
+ owner_field: str | None = None,
121
+ ) -> None:
122
+ self.model_class = model_class
123
+ self.plural = plural
124
+ self.soft_delete = soft_delete
125
+ self.audit = audit
126
+ self.lookup_field = lookup_field
127
+ self.access = access
128
+ self.scope = scope
129
+ self.tenant_field = tenant_field
130
+ self.owner_field = owner_field
131
+ self.permissions = self._resolve_permissions(readonly, exclude, only)
132
+
133
+ def _resolve_permissions(
134
+ self,
135
+ readonly: bool,
136
+ exclude: list[str] | None,
137
+ only: list[str] | None,
138
+ ) -> list[str]:
139
+ if only:
140
+ return [op for op in only if op in ALL_OPERATIONS]
141
+ if readonly:
142
+ return ["list", "read"]
143
+ if exclude:
144
+ return [op for op in ALL_OPERATIONS if op not in exclude]
145
+ return list(ALL_OPERATIONS)