dynamicforms-fastapi-viewsets 0.12.1__tar.gz → 0.13.0__tar.gz

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 (70) hide show
  1. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/PKG-INFO +1 -1
  2. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/__init__.py +1 -1
  3. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/mixins.py +79 -36
  4. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/.gitignore +0 -0
  5. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/LICENSE +0 -0
  6. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/README.md +0 -0
  7. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/action_configuration.py +0 -0
  8. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/backends/__init__.py +0 -0
  9. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/backends/django_orm.py +0 -0
  10. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/collection_viewset.py +0 -0
  11. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/conf.py +0 -0
  12. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/context/__init__.py +0 -0
  13. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/context/auth/__init__.py +0 -0
  14. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/context/auth/django.py +0 -0
  15. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/context/auth/jwt.py +0 -0
  16. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/context/auth/static.py +0 -0
  17. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/cursor.py +0 -0
  18. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/decorators/__init__.py +0 -0
  19. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/decorators/build_schema.py +0 -0
  20. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/decorators/celery_viewset/__init__.py +0 -0
  21. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/decorators/celery_viewset/client.py +0 -0
  22. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/decorators/celery_viewset/payload.py +0 -0
  23. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/decorators/celery_viewset/result_reader.py +0 -0
  24. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/decorators/celery_viewset/server.py +0 -0
  25. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/decorators/conftest.py +0 -0
  26. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/decorators/lifecycle_runner.py +0 -0
  27. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/decorators/primary_key_model_helper.py +0 -0
  28. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/decorators/route_viewset.py +0 -0
  29. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/endpoint_docs.py +0 -0
  30. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/error_namespace.py +0 -0
  31. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/error_responses.py +0 -0
  32. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/events/__init__.py +0 -0
  33. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/events/authorizers.py +0 -0
  34. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/events/changes.py +0 -0
  35. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/events/conftest.py +0 -0
  36. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/events/delivery.py +0 -0
  37. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/events/event.py +0 -0
  38. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/events/lifecycle.py +0 -0
  39. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/events/redis.py +0 -0
  40. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/events/scopes.py +0 -0
  41. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/events/sources.py +0 -0
  42. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/events/store.py +0 -0
  43. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/events/topics.py +0 -0
  44. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/events/viewset.py +0 -0
  45. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/exceptions.py +0 -0
  46. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/filters/__init__.py +0 -0
  47. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/filters/base.py +0 -0
  48. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/filters/declaration.py +0 -0
  49. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/filters/operators.py +0 -0
  50. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/filters/registry.py +0 -0
  51. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/list_query.py +0 -0
  52. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/list_shapes.py +0 -0
  53. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/middleware/__init__.py +0 -0
  54. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/middleware/auth/__init__.py +0 -0
  55. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/middleware/auth/authorization.py +0 -0
  56. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/middleware/idempotency.py +0 -0
  57. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/middleware/rate_limiter.py +0 -0
  58. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/mux_ws/__init__.py +0 -0
  59. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/mux_ws/asgi.py +0 -0
  60. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/mux_ws/protocol.py +0 -0
  61. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/mux_ws/registry.py +0 -0
  62. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/mux_ws/server.py +0 -0
  63. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/mux_ws/transports.py +0 -0
  64. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/py.typed +0 -0
  65. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/response_classes.py +0 -0
  66. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/routing.py +0 -0
  67. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/save_state/__init__.py +0 -0
  68. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/save_state/save_state.py +0 -0
  69. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/fastapi_viewsets/save_state/serialize_state.py +0 -0
  70. {dynamicforms_fastapi_viewsets-0.12.1 → dynamicforms_fastapi_viewsets-0.13.0}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: dynamicforms-fastapi-viewsets
3
- Version: 0.12.1
3
+ Version: 0.13.0
4
4
  Summary: Django REST Framework-style viewsets for FastAPI, with optional Celery-backed async execution and a Vue/TypeScript client counterpart.
5
5
  Project-URL: Homepage, https://github.com/dynamicforms/fastapi-viewsets
6
6
  Project-URL: Repository, https://github.com/dynamicforms/fastapi-viewsets
@@ -1,4 +1,4 @@
1
- __version__ = "0.12.1"
1
+ __version__ = "0.13.0"
2
2
 
3
3
  from .error_namespace import ERROR_NAMESPACE, ERROR_NAMESPACE_PYDANTIC
4
4
 
@@ -775,73 +775,116 @@ class LookupFilter(BaseModel):
775
775
 
776
776
 
777
777
  TLookupFilter = TypeVar("TLookupFilter", default=LookupFilter)
778
+ TLookupItem = TypeVar("TLookupItem", default=LookupItem)
778
779
 
779
780
 
780
- class LookupMixin(Generic[TLookupFilter], ABC):
781
+ class LookupMixin(Generic[TLookupFilter, TLookupItem], ABC):
781
782
  """
782
783
  Lookup endpoint with optional search filtering.
783
784
 
784
- Adds a GET /lookup endpoint that returns a list of LookupItem objects — useful for populating
785
- select/autocomplete widgets.
785
+ Adds a GET /lookup endpoint that returns a list of TLookupItem objects (default: LookupItem) -
786
+ useful for populating select/autocomplete widgets.
786
787
 
787
- The mixin is generic over TLookupFilter (default: LookupFilter). When no type argument is
788
- given, the endpoint exposes a single 'q' query parameter and filter_lookup filters by
789
- case-insensitive title match. Supply a custom filter model as a type argument to add extra
790
- query parameters and override filter_lookup.
788
+ The fields of TLookupFilter (default: LookupFilter, a single `q`) are the endpoint's query
789
+ parameters. The request runs through the same stages as a list:
791
790
 
792
- When the filter is active (any field is non-None), the following two-phase pipeline runs:
791
+ 1. `perform_lookup(context)` returns the source, lazily or not.
792
+ 2. `apply_lookup_filter(context, query, records)` narrows it. Override it to filter in the
793
+ backend and mark the stage applied (`query.mark_applied("filter")`); the default filters in
794
+ memory: by the declaration of a filter model built with `make_filter_model`, otherwise
795
+ through `filter_lookup`.
796
+ 3. `to_lookup_item(raw)` converts each remaining row to TLookupItem.
793
797
 
794
- 1. Pre-filter: setup_lookup_filter is called before perform_lookup.
795
- Override to apply server-side filtering (e.g., narrow a DB query).
796
- 2. Post-filter: filter_lookup is called after perform_lookup.
797
- The default implementation filters by fltr.q (case-insensitive title match).
798
- Override to customise in-memory filtering.
798
+ The filter travels in `query.fltr`, never on the viewset instance, which serves concurrent
799
+ requests under the default `singleton` lifecycle.
799
800
  """
800
801
 
801
802
  __router = APIRouter()
802
803
 
803
804
  @abstractmethod
804
- async def perform_lookup(self, context: Context) -> list[LookupItem]:
805
+ async def perform_lookup(self, context: Context) -> ListRecords:
805
806
  """
806
- Subclasses should implement this method to return lookup items.
807
- This method is intentionally NOT in ImplMixin because it's expected to be a simple
808
- transformation of perform_list.
807
+ Subclasses implement this to return the lookup source.
808
+
809
+ A lazy source - a generator, an unevaluated QuerySet - is what an overridden
810
+ `apply_lookup_filter` pushes the filter into. Rows are TLookupItem once `to_lookup_item` has
811
+ run over them.
809
812
  """
810
813
  raise NotImplementedError("Method 'perform_lookup' must be implemented.")
811
814
 
812
- async def setup_lookup_filter(self, fltr: TLookupFilter) -> None:
815
+ async def get_lookup(self, context: Context, query: ListQuery) -> list[TLookupItem]:
816
+ """The lookup pipeline: source, filter, conversion. Overridable as a whole."""
817
+ records = await self.perform_lookup(context)
818
+ records = await self.apply_lookup_filter(context, query, records)
819
+ return await self.land_lookup(query, records)
820
+
821
+ async def apply_lookup_filter(
822
+ self,
823
+ context: Context, # noqa: ARG002 - part of the hook signature, for overrides to use
824
+ query: ListQuery,
825
+ records: ListRecords,
826
+ ) -> ListRecords:
813
827
  """
814
- Optional pre-filter hook called before perform_lookup when the filter is active.
815
- Subclasses can override to set up server-side filtering (e.g., build a DB query).
828
+ Narrows `records` to those the filter accepts. Override to filter in the backend; mark the
829
+ stage applied so this default does not redo the work:
830
+
831
+ async def apply_lookup_filter(self, context, query, records):
832
+ if query.has_filter:
833
+ records = records.filter(pk__in=query.fltr.ids)
834
+ query.mark_applied("filter")
835
+ return await super().apply_lookup_filter(context, query, records)
836
+
837
+ The default filters TLookupItem records in memory: by the declaration of a filter model built
838
+ with `make_filter_model`, otherwise through the `filter_lookup` hook.
816
839
  """
840
+ if not query.has_filter or not query.needs("filter"):
841
+ return records
817
842
 
818
- async def filter_lookup(self, fltr: TLookupFilter, items: list[LookupItem]) -> list[LookupItem]:
843
+ items = await self.land_lookup(query, records)
844
+ declared = filters_from(query.fltr)
845
+ if declared is not None:
846
+ return declared.apply(items)
847
+ filtered = await self.filter_lookup(query.fltr, items)
848
+ return items if filtered is None else filtered
849
+
850
+ async def filter_lookup(self, fltr: TLookupFilter, items: list[TLookupItem]) -> list[TLookupItem]:
819
851
  """
820
- Post-filter hook called after perform_lookup when the filter is active.
821
- Default implementation filters by fltr.q (case-insensitive substring of title).
822
- Subclasses can override to change the filtering behaviour.
852
+ In-memory filter hook of a hand-written filter model, called with the converted items.
853
+ The default filters by `fltr.q`: a case-insensitive substring of the item's `title`.
823
854
  """
824
855
  q = getattr(fltr, "q", None)
825
856
  if q is None:
826
857
  return items
827
- return [item for item in items if q.lower() in item.title.lower()]
858
+ needle = q.lower()
859
+ return [item for item in items if needle in str(getattr(item, "title", "")).lower()]
860
+
861
+ def to_lookup_item(self, raw: Any) -> TLookupItem:
862
+ """
863
+ Converts one row of the source into TLookupItem. The default is identity, for a source that
864
+ already yields TLookupItem.
865
+ """
866
+ return raw
867
+
868
+ async def land_lookup(self, query: ListQuery, records: ListRecords) -> list[TLookupItem]:
869
+ """
870
+ Materialises the source and converts it with `to_lookup_item`. Idempotent: the conversion
871
+ runs once per request, whichever stage lands the source first.
872
+ """
873
+ materialized = await materialize(records)
874
+ if query.needs("conversion"):
875
+ query.mark_applied("conversion")
876
+ if type(self).to_lookup_item is not LookupMixin.to_lookup_item:
877
+ return [self.to_lookup_item(row) for row in materialized]
878
+ return materialized
828
879
 
829
880
  @final
830
881
  @__router.get("lookup")
831
882
  async def lookup(
832
- self: "ImplMixin[Any, LookupItem] | LookupMixin",
883
+ self: "LookupMixin",
833
884
  context: Context,
834
885
  fltr: Annotated[TLookupFilter, Query()] = None,
835
- ) -> list[LookupItem]:
836
- has_filter = (
837
- fltr is not None and hasattr(fltr, "model_dump") and any(v is not None for v in fltr.model_dump().values())
838
- )
839
- if has_filter:
840
- await self.setup_lookup_filter(fltr)
841
- res = await self.perform_lookup(context)
842
- if has_filter:
843
- res = await self.filter_lookup(fltr, res)
844
- return res
886
+ ) -> list[TLookupItem]:
887
+ return await self.get_lookup(context, build_list_query(fltr, None))
845
888
 
846
889
 
847
890
  ###################################################################################################