litestar-vite 0.1.1__py3-none-any.whl → 0.15.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 (169) hide show
  1. litestar_vite/__init__.py +54 -4
  2. litestar_vite/__metadata__.py +12 -7
  3. litestar_vite/cli.py +1048 -10
  4. litestar_vite/codegen/__init__.py +48 -0
  5. litestar_vite/codegen/_export.py +229 -0
  6. litestar_vite/codegen/_inertia.py +619 -0
  7. litestar_vite/codegen/_openapi.py +280 -0
  8. litestar_vite/codegen/_routes.py +720 -0
  9. litestar_vite/codegen/_ts.py +235 -0
  10. litestar_vite/codegen/_utils.py +141 -0
  11. litestar_vite/commands.py +73 -0
  12. litestar_vite/config/__init__.py +997 -0
  13. litestar_vite/config/_constants.py +97 -0
  14. litestar_vite/config/_deploy.py +70 -0
  15. litestar_vite/config/_inertia.py +241 -0
  16. litestar_vite/config/_paths.py +63 -0
  17. litestar_vite/config/_runtime.py +235 -0
  18. litestar_vite/config/_spa.py +93 -0
  19. litestar_vite/config/_types.py +94 -0
  20. litestar_vite/deploy.py +366 -0
  21. litestar_vite/doctor.py +1181 -0
  22. litestar_vite/exceptions.py +78 -0
  23. litestar_vite/executor.py +360 -0
  24. litestar_vite/handler/__init__.py +9 -0
  25. litestar_vite/handler/_app.py +612 -0
  26. litestar_vite/handler/_routing.py +130 -0
  27. litestar_vite/html_transform.py +569 -0
  28. litestar_vite/inertia/__init__.py +77 -0
  29. litestar_vite/inertia/_utils.py +119 -0
  30. litestar_vite/inertia/exception_handler.py +178 -0
  31. litestar_vite/inertia/helpers.py +1571 -0
  32. litestar_vite/inertia/middleware.py +54 -0
  33. litestar_vite/inertia/plugin.py +199 -0
  34. litestar_vite/inertia/precognition.py +274 -0
  35. litestar_vite/inertia/request.py +334 -0
  36. litestar_vite/inertia/response.py +802 -0
  37. litestar_vite/inertia/types.py +335 -0
  38. litestar_vite/loader.py +464 -123
  39. litestar_vite/plugin/__init__.py +687 -0
  40. litestar_vite/plugin/_process.py +185 -0
  41. litestar_vite/plugin/_proxy.py +689 -0
  42. litestar_vite/plugin/_proxy_headers.py +244 -0
  43. litestar_vite/plugin/_static.py +37 -0
  44. litestar_vite/plugin/_utils.py +489 -0
  45. litestar_vite/py.typed +0 -0
  46. litestar_vite/scaffolding/__init__.py +20 -0
  47. litestar_vite/scaffolding/generator.py +270 -0
  48. litestar_vite/scaffolding/templates.py +437 -0
  49. litestar_vite/templates/__init__.py +0 -0
  50. litestar_vite/templates/addons/tailwindcss/tailwind.css.j2 +1 -0
  51. litestar_vite/templates/angular/index.html.j2 +12 -0
  52. litestar_vite/templates/angular/openapi-ts.config.ts.j2 +18 -0
  53. litestar_vite/templates/angular/package.json.j2 +36 -0
  54. litestar_vite/templates/angular/src/app/app.component.css.j2 +3 -0
  55. litestar_vite/templates/angular/src/app/app.component.html.j2 +1 -0
  56. litestar_vite/templates/angular/src/app/app.component.ts.j2 +9 -0
  57. litestar_vite/templates/angular/src/app/app.config.ts.j2 +5 -0
  58. litestar_vite/templates/angular/src/main.ts.j2 +9 -0
  59. litestar_vite/templates/angular/src/styles.css.j2 +9 -0
  60. litestar_vite/templates/angular/tsconfig.app.json.j2 +34 -0
  61. litestar_vite/templates/angular/tsconfig.json.j2 +20 -0
  62. litestar_vite/templates/angular/vite.config.ts.j2 +21 -0
  63. litestar_vite/templates/angular-cli/.postcssrc.json.j2 +5 -0
  64. litestar_vite/templates/angular-cli/angular.json.j2 +36 -0
  65. litestar_vite/templates/angular-cli/openapi-ts.config.ts.j2 +18 -0
  66. litestar_vite/templates/angular-cli/package.json.j2 +28 -0
  67. litestar_vite/templates/angular-cli/proxy.conf.json.j2 +18 -0
  68. litestar_vite/templates/angular-cli/src/app/app.component.css.j2 +3 -0
  69. litestar_vite/templates/angular-cli/src/app/app.component.html.j2 +1 -0
  70. litestar_vite/templates/angular-cli/src/app/app.component.ts.j2 +9 -0
  71. litestar_vite/templates/angular-cli/src/app/app.config.ts.j2 +5 -0
  72. litestar_vite/templates/angular-cli/src/index.html.j2 +13 -0
  73. litestar_vite/templates/angular-cli/src/main.ts.j2 +6 -0
  74. litestar_vite/templates/angular-cli/src/styles.css.j2 +10 -0
  75. litestar_vite/templates/angular-cli/tailwind.config.js.j2 +4 -0
  76. litestar_vite/templates/angular-cli/tsconfig.app.json.j2 +16 -0
  77. litestar_vite/templates/angular-cli/tsconfig.json.j2 +26 -0
  78. litestar_vite/templates/angular-cli/tsconfig.spec.json.j2 +9 -0
  79. litestar_vite/templates/astro/astro.config.mjs.j2 +28 -0
  80. litestar_vite/templates/astro/openapi-ts.config.ts.j2 +15 -0
  81. litestar_vite/templates/astro/src/layouts/Layout.astro.j2 +63 -0
  82. litestar_vite/templates/astro/src/pages/index.astro.j2 +36 -0
  83. litestar_vite/templates/astro/src/styles/global.css.j2 +1 -0
  84. litestar_vite/templates/base/.gitignore.j2 +42 -0
  85. litestar_vite/templates/base/openapi-ts.config.ts.j2 +15 -0
  86. litestar_vite/templates/base/package.json.j2 +39 -0
  87. litestar_vite/templates/base/resources/vite-env.d.ts.j2 +1 -0
  88. litestar_vite/templates/base/tsconfig.json.j2 +37 -0
  89. litestar_vite/templates/htmx/src/main.js.j2 +8 -0
  90. litestar_vite/templates/htmx/templates/base.html.j2.j2 +56 -0
  91. litestar_vite/templates/htmx/templates/index.html.j2.j2 +13 -0
  92. litestar_vite/templates/htmx/vite.config.ts.j2 +40 -0
  93. litestar_vite/templates/nuxt/app.vue.j2 +29 -0
  94. litestar_vite/templates/nuxt/composables/useApi.ts.j2 +33 -0
  95. litestar_vite/templates/nuxt/nuxt.config.ts.j2 +31 -0
  96. litestar_vite/templates/nuxt/openapi-ts.config.ts.j2 +15 -0
  97. litestar_vite/templates/nuxt/pages/index.vue.j2 +54 -0
  98. litestar_vite/templates/react/index.html.j2 +13 -0
  99. litestar_vite/templates/react/src/App.css.j2 +56 -0
  100. litestar_vite/templates/react/src/App.tsx.j2 +19 -0
  101. litestar_vite/templates/react/src/main.tsx.j2 +10 -0
  102. litestar_vite/templates/react/vite.config.ts.j2 +39 -0
  103. litestar_vite/templates/react-inertia/index.html.j2 +14 -0
  104. litestar_vite/templates/react-inertia/package.json.j2 +47 -0
  105. litestar_vite/templates/react-inertia/resources/App.css.j2 +68 -0
  106. litestar_vite/templates/react-inertia/resources/main.tsx.j2 +17 -0
  107. litestar_vite/templates/react-inertia/resources/pages/Home.tsx.j2 +18 -0
  108. litestar_vite/templates/react-inertia/resources/ssr.tsx.j2 +19 -0
  109. litestar_vite/templates/react-inertia/vite.config.ts.j2 +59 -0
  110. litestar_vite/templates/react-router/index.html.j2 +12 -0
  111. litestar_vite/templates/react-router/src/App.css.j2 +17 -0
  112. litestar_vite/templates/react-router/src/App.tsx.j2 +7 -0
  113. litestar_vite/templates/react-router/src/main.tsx.j2 +10 -0
  114. litestar_vite/templates/react-router/vite.config.ts.j2 +39 -0
  115. litestar_vite/templates/react-tanstack/index.html.j2 +12 -0
  116. litestar_vite/templates/react-tanstack/openapi-ts.config.ts.j2 +18 -0
  117. litestar_vite/templates/react-tanstack/src/App.css.j2 +17 -0
  118. litestar_vite/templates/react-tanstack/src/main.tsx.j2 +21 -0
  119. litestar_vite/templates/react-tanstack/src/routeTree.gen.ts.j2 +7 -0
  120. litestar_vite/templates/react-tanstack/src/routes/__root.tsx.j2 +9 -0
  121. litestar_vite/templates/react-tanstack/src/routes/books.tsx.j2 +9 -0
  122. litestar_vite/templates/react-tanstack/src/routes/index.tsx.j2 +9 -0
  123. litestar_vite/templates/react-tanstack/vite.config.ts.j2 +39 -0
  124. litestar_vite/templates/svelte/index.html.j2 +13 -0
  125. litestar_vite/templates/svelte/src/App.svelte.j2 +30 -0
  126. litestar_vite/templates/svelte/src/app.css.j2 +45 -0
  127. litestar_vite/templates/svelte/src/main.ts.j2 +8 -0
  128. litestar_vite/templates/svelte/src/vite-env.d.ts.j2 +2 -0
  129. litestar_vite/templates/svelte/svelte.config.js.j2 +5 -0
  130. litestar_vite/templates/svelte/vite.config.ts.j2 +39 -0
  131. litestar_vite/templates/svelte-inertia/index.html.j2 +14 -0
  132. litestar_vite/templates/svelte-inertia/resources/app.css.j2 +21 -0
  133. litestar_vite/templates/svelte-inertia/resources/main.ts.j2 +11 -0
  134. litestar_vite/templates/svelte-inertia/resources/pages/Home.svelte.j2 +43 -0
  135. litestar_vite/templates/svelte-inertia/resources/vite-env.d.ts.j2 +2 -0
  136. litestar_vite/templates/svelte-inertia/svelte.config.js.j2 +5 -0
  137. litestar_vite/templates/svelte-inertia/vite.config.ts.j2 +37 -0
  138. litestar_vite/templates/sveltekit/openapi-ts.config.ts.j2 +15 -0
  139. litestar_vite/templates/sveltekit/src/app.css.j2 +40 -0
  140. litestar_vite/templates/sveltekit/src/app.html.j2 +12 -0
  141. litestar_vite/templates/sveltekit/src/hooks.server.ts.j2 +55 -0
  142. litestar_vite/templates/sveltekit/src/routes/+layout.svelte.j2 +12 -0
  143. litestar_vite/templates/sveltekit/src/routes/+page.svelte.j2 +34 -0
  144. litestar_vite/templates/sveltekit/svelte.config.js.j2 +12 -0
  145. litestar_vite/templates/sveltekit/tsconfig.json.j2 +14 -0
  146. litestar_vite/templates/sveltekit/vite.config.ts.j2 +31 -0
  147. litestar_vite/templates/vue/env.d.ts.j2 +7 -0
  148. litestar_vite/templates/vue/index.html.j2 +13 -0
  149. litestar_vite/templates/vue/src/App.vue.j2 +28 -0
  150. litestar_vite/templates/vue/src/main.ts.j2 +5 -0
  151. litestar_vite/templates/vue/src/style.css.j2 +45 -0
  152. litestar_vite/templates/vue/vite.config.ts.j2 +39 -0
  153. litestar_vite/templates/vue-inertia/env.d.ts.j2 +7 -0
  154. litestar_vite/templates/vue-inertia/index.html.j2 +14 -0
  155. litestar_vite/templates/vue-inertia/package.json.j2 +50 -0
  156. litestar_vite/templates/vue-inertia/resources/main.ts.j2 +18 -0
  157. litestar_vite/templates/vue-inertia/resources/pages/Home.vue.j2 +22 -0
  158. litestar_vite/templates/vue-inertia/resources/ssr.ts.j2 +21 -0
  159. litestar_vite/templates/vue-inertia/resources/style.css.j2 +21 -0
  160. litestar_vite/templates/vue-inertia/vite.config.ts.j2 +59 -0
  161. litestar_vite-0.15.0.dist-info/METADATA +230 -0
  162. litestar_vite-0.15.0.dist-info/RECORD +164 -0
  163. {litestar_vite-0.1.1.dist-info → litestar_vite-0.15.0.dist-info}/WHEEL +1 -1
  164. litestar_vite/config.py +0 -100
  165. litestar_vite/plugin.py +0 -45
  166. litestar_vite/template_engine.py +0 -103
  167. litestar_vite-0.1.1.dist-info/METADATA +0 -68
  168. litestar_vite-0.1.1.dist-info/RECORD +0 -11
  169. {litestar_vite-0.1.1.dist-info → litestar_vite-0.15.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,334 @@
1
+ from functools import cached_property
2
+ from typing import TYPE_CHECKING, cast
3
+ from urllib.parse import unquote
4
+
5
+ from litestar import Request
6
+ from litestar.connection.base import AuthT, StateT, UserT, empty_receive, empty_send
7
+
8
+ from litestar_vite.inertia._utils import InertiaHeaders
9
+
10
+ if TYPE_CHECKING:
11
+ from litestar.types import Receive, Scope, Send
12
+
13
+ from litestar_vite.inertia.plugin import InertiaPlugin
14
+
15
+ __all__ = ("InertiaDetails", "InertiaHeaders", "InertiaRequest")
16
+
17
+ _DEFAULT_COMPONENT_OPT_KEYS: "tuple[str, ...]" = ("component", "page")
18
+
19
+
20
+ class InertiaDetails:
21
+ """InertiaDetails holds all the values sent by Inertia client in headers and provide convenient properties."""
22
+
23
+ def __init__(self, request: "Request[UserT, AuthT, StateT]") -> None:
24
+ """Initialize :class:`InertiaDetails`"""
25
+ self.request = request
26
+
27
+ def _get_header_value(self, name: "InertiaHeaders") -> "str | None":
28
+ """Parse request header
29
+
30
+ Check for uri encoded header and unquotes it in readable format.
31
+
32
+ Args:
33
+ name: The header name.
34
+
35
+ Returns:
36
+ The header value.
37
+ """
38
+
39
+ if value := self.request.headers.get(name.value.lower()):
40
+ is_uri_encoded = self.request.headers.get(f"{name.value.lower()}-uri-autoencoded") == "true"
41
+ return unquote(value) if is_uri_encoded else value
42
+ return None
43
+
44
+ def _get_route_component(self) -> "str | None":
45
+ """Return the route component from handler opts if present.
46
+
47
+ Returns:
48
+ The route component name, or None if not configured on the handler.
49
+ """
50
+ rh = self.request.scope.get("route_handler") # pyright: ignore[reportUnknownMemberType]
51
+ if rh:
52
+ component_opt_keys: "tuple[str, ...]" = _DEFAULT_COMPONENT_OPT_KEYS
53
+ try:
54
+ inertia_plugin: "InertiaPlugin" = self.request.app.plugins.get("InertiaPlugin")
55
+ component_opt_keys = inertia_plugin.config.component_opt_keys
56
+ except KeyError:
57
+ pass
58
+
59
+ for key in component_opt_keys:
60
+ if (value := rh.opt.get(key)) is not None:
61
+ return cast("str", value)
62
+ return None
63
+
64
+ def __bool__(self) -> bool:
65
+ """Return True when the request is sent by an Inertia client.
66
+
67
+ Returns:
68
+ True if the request originated from an Inertia client, otherwise False.
69
+ """
70
+ return self._get_header_value(InertiaHeaders.ENABLED) == "true"
71
+
72
+ @cached_property
73
+ def route_component(self) -> "str | None":
74
+ """Return the route component name.
75
+
76
+ Returns:
77
+ The route component name, or None if not configured.
78
+ """
79
+ return self._get_route_component()
80
+
81
+ @cached_property
82
+ def partial_component(self) -> "str | None":
83
+ """Return the partial component name from headers.
84
+
85
+ Returns:
86
+ The partial component name, or None if not present.
87
+ """
88
+ return self._get_header_value(InertiaHeaders.PARTIAL_COMPONENT)
89
+
90
+ @cached_property
91
+ def partial_data(self) -> "str | None":
92
+ """Return partial-data keys requested by the client.
93
+
94
+ Returns:
95
+ Comma-separated partial-data keys, or None if not present.
96
+ """
97
+ return self._get_header_value(InertiaHeaders.PARTIAL_DATA)
98
+
99
+ @cached_property
100
+ def partial_except(self) -> "str | None":
101
+ """Return partial-except keys requested by the client.
102
+
103
+ Returns:
104
+ Comma-separated partial-except keys, or None if not present.
105
+ """
106
+ return self._get_header_value(InertiaHeaders.PARTIAL_EXCEPT)
107
+
108
+ @cached_property
109
+ def reset_props(self) -> "str | None":
110
+ """Return comma-separated props to reset on navigation.
111
+
112
+ Returns:
113
+ Comma-separated prop keys to reset, or None if not present.
114
+ """
115
+ return self._get_header_value(InertiaHeaders.RESET)
116
+
117
+ @cached_property
118
+ def error_bag(self) -> "str | None":
119
+ """Return the error bag name for scoped validation errors.
120
+
121
+ Returns:
122
+ The error bag name, or None if not present.
123
+ """
124
+ return self._get_header_value(InertiaHeaders.ERROR_BAG)
125
+
126
+ @cached_property
127
+ def merge_intent(self) -> "str | None":
128
+ """Return infinite-scroll merge intent (append/prepend).
129
+
130
+ Returns:
131
+ The merge intent string, or None if not present.
132
+ """
133
+ return self._get_header_value(InertiaHeaders.INFINITE_SCROLL_MERGE_INTENT)
134
+
135
+ @cached_property
136
+ def version(self) -> "str | None":
137
+ """Return the Inertia asset version sent by the client.
138
+
139
+ Returns:
140
+ The version string, or None if not present.
141
+ """
142
+ return self._get_header_value(InertiaHeaders.VERSION)
143
+
144
+ @cached_property
145
+ def referer(self) -> "str | None":
146
+ """Return the referer value if present.
147
+
148
+ Returns:
149
+ The referer value, or None if not present.
150
+ """
151
+ return self._get_header_value(InertiaHeaders.REFERER)
152
+
153
+ @cached_property
154
+ def is_partial_render(self) -> bool:
155
+ """Return True when the request is a partial render.
156
+
157
+ Returns:
158
+ True if the request is a partial render, otherwise False.
159
+ """
160
+ return bool(self.partial_component == self.route_component and (self.partial_data or self.partial_except))
161
+
162
+ @cached_property
163
+ def partial_keys(self) -> list[str]:
164
+ """Return parsed partial-data keys.
165
+
166
+ Returns:
167
+ Parsed partial-data keys.
168
+ """
169
+ return self.partial_data.split(",") if self.partial_data is not None else []
170
+
171
+ @cached_property
172
+ def partial_except_keys(self) -> list[str]:
173
+ """Return parsed partial-except keys (takes precedence over partial_keys).
174
+
175
+ Returns:
176
+ Parsed partial-except keys.
177
+ """
178
+ return self.partial_except.split(",") if self.partial_except is not None else []
179
+
180
+ @cached_property
181
+ def reset_keys(self) -> list[str]:
182
+ """Return parsed reset keys from headers.
183
+
184
+ Returns:
185
+ Parsed reset keys.
186
+ """
187
+ return self.reset_props.split(",") if self.reset_props is not None else []
188
+
189
+ @cached_property
190
+ def is_precognition(self) -> bool:
191
+ """Return True when the request is a Precognition validation request.
192
+
193
+ Precognition requests allow real-time form validation without
194
+ executing handler side effects.
195
+
196
+ Returns:
197
+ True if Precognition header is present and "true".
198
+ """
199
+ return self._get_header_value(InertiaHeaders.PRECOGNITION) == "true"
200
+
201
+ @cached_property
202
+ def precognition_validate_only(self) -> list[str]:
203
+ """Return the fields to validate for partial Precognition validation.
204
+
205
+ When present, only errors for these fields should be returned.
206
+
207
+ Returns:
208
+ List of field names to validate, or empty list if validating all fields.
209
+ """
210
+ value = self._get_header_value(InertiaHeaders.PRECOGNITION_VALIDATE_ONLY)
211
+ return value.split(",") if value else []
212
+
213
+
214
+ class InertiaRequest(Request[UserT, AuthT, StateT]):
215
+ """Inertia Request class to work with Inertia client."""
216
+
217
+ __slots__ = ("inertia",)
218
+
219
+ def __init__(self, scope: "Scope", receive: "Receive" = empty_receive, send: "Send" = empty_send) -> None:
220
+ """Initialize :class:`InertiaRequest`"""
221
+ super().__init__(scope=scope, receive=receive, send=send)
222
+ self.inertia = InertiaDetails(self)
223
+
224
+ @property
225
+ def is_inertia(self) -> bool:
226
+ """True if the request contained inertia headers.
227
+
228
+ Returns:
229
+ True if the request contains Inertia headers, otherwise False.
230
+ """
231
+ return bool(self.inertia)
232
+
233
+ @property
234
+ def inertia_enabled(self) -> bool:
235
+ """True if the route handler contains an inertia enabled configuration.
236
+
237
+ Returns:
238
+ True if the route is configured with an Inertia component, otherwise False.
239
+ """
240
+ return bool(self.inertia.route_component is not None)
241
+
242
+ @property
243
+ def is_partial_render(self) -> bool:
244
+ """True if the request is a partial reload.
245
+
246
+ Returns:
247
+ True if the request is a partial reload, otherwise False.
248
+ """
249
+ return self.inertia.is_partial_render
250
+
251
+ @property
252
+ def partial_keys(self) -> "set[str]":
253
+ """Get the props to include in partial render.
254
+
255
+ Returns:
256
+ A set of prop keys to include.
257
+ """
258
+ return set(self.inertia.partial_keys)
259
+
260
+ @property
261
+ def partial_except_keys(self) -> "set[str]":
262
+ """Get the props to exclude from partial render (v2).
263
+
264
+ Takes precedence over partial_keys if both present.
265
+
266
+ Returns:
267
+ A set of prop keys to exclude.
268
+ """
269
+ return set(self.inertia.partial_except_keys)
270
+
271
+ @property
272
+ def reset_keys(self) -> "set[str]":
273
+ """Get the props to reset on navigation (v2).
274
+
275
+ Returns:
276
+ A set of prop keys to reset.
277
+ """
278
+ return set(self.inertia.reset_keys)
279
+
280
+ @property
281
+ def error_bag(self) -> "str | None":
282
+ """Get the error bag name for scoped validation errors (v2).
283
+
284
+ Returns:
285
+ The error bag name, or None if not present.
286
+ """
287
+ return self.inertia.error_bag
288
+
289
+ @property
290
+ def merge_intent(self) -> "str | None":
291
+ """Get the infinite scroll merge intent (v2).
292
+
293
+ Returns 'append' or 'prepend' for infinite scroll merging.
294
+
295
+ Returns:
296
+ The merge intent string, or None if not present.
297
+ """
298
+ return self.inertia.merge_intent
299
+
300
+ @property
301
+ def inertia_version(self) -> "str | None":
302
+ """Get the Inertia asset version sent by the client.
303
+
304
+ The client sends this header so the server can detect version mismatches
305
+ and trigger a hard refresh when assets have changed.
306
+
307
+ Returns:
308
+ The version string sent by the client, or None if not present.
309
+ """
310
+ return self.inertia.version
311
+
312
+ @property
313
+ def is_precognition(self) -> bool:
314
+ """Check if this is a Precognition validation request.
315
+
316
+ Precognition requests run validation without executing the handler body,
317
+ enabling real-time form validation.
318
+
319
+ Returns:
320
+ True if this is a Precognition request.
321
+ """
322
+ return self.inertia.is_precognition
323
+
324
+ @property
325
+ def precognition_validate_only(self) -> "set[str]":
326
+ """Get fields to validate for partial Precognition validation.
327
+
328
+ When present, only validation errors for these fields should be returned.
329
+ Used for validating individual fields as the user types.
330
+
331
+ Returns:
332
+ A set of field names to validate, or empty set if validating all.
333
+ """
334
+ return set(self.inertia.precognition_validate_only)