fastlifeweb 0.18.0__py3-none-any.whl → 0.20.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.
CHANGELOG.md ADDED
@@ -0,0 +1,206 @@
1
+ ## 0.20.0 - Released on 2024-11-09
2
+ * Add a new class GenericRegistry in order to properly type custom Configurator / Registry / Settings
3
+ * Using InlineTemplate, we can pass arbitrary types for pydantic form
4
+
5
+ ## 0.19.0 - Released on 2024-11-07
6
+ * Drop Babel from depenencies for i18n, rely on GNUTranslations only
7
+ * Change License to MIT
8
+ * Replace poetry by uv/pdm
9
+ * Update CI workflows
10
+
11
+ ## 0.18.0 - Released on 2024-10-13
12
+ * Make the sphinx pluging {mod}`fastlife.adapters.jinjax.jinjax_ext.jinjax_doc`
13
+ parts from the API in order to let users build their own component documentation.
14
+
15
+ ## 0.17.0 - Released on 2024-10-08
16
+ * Fix @configure decorator signature for GenericConfigurator
17
+ * Breaking change - rename Configurator.set_open_tag to Configurator.set_openapi_tag
18
+
19
+ ## 0.16.4 - Released on 2024-10-04
20
+ * Add support of x-real-port for port detection, fallback port to 0 instead of None if missing
21
+
22
+ ## 0.16.3 - Released on 2024-10-03
23
+ * Fix middleware that process the x-forwarded-headers to respect ASGI spec for client
24
+
25
+ ## 0.16.2 - Released on 2024-10-03
26
+ * Add a new property all_registered_permissions on the Configurator class
27
+
28
+ ## 0.16.1 - Released on 2024-10-03
29
+ * Fix import in the SecurityPolicy that make it unusable.
30
+
31
+ ## 0.16.0 - Released on 2024-10-02
32
+ * Make the Configurator, Request and Registry Generic.
33
+ * Breaking change, remove settings `api_swagger_ui_url` and `api_redoc_url`
34
+ now to register those url, use
35
+ {meth}`fastlife.config.configurator.GenericConfigurator.set_api_documentation_info`
36
+ * Breaking change, in the method
37
+ {meth}`fastlife.config.configurator.GenericConfigurator.set_api_documentation_info`
38
+ summary is now kwargs only.
39
+
40
+ ## 0.15.1 - Released on 2024-09-29
41
+ * Hotfix components to create tables
42
+
43
+ ## 0.15.0 - Released on 2024-09-29
44
+ * Add an {class}`fastlife.security.policy.AbstractSecurityPolicy` class
45
+ * New method {meth}`fastlife.config.configurator.GenericConfigurator.set_security_policy`
46
+ * Breaking change, the check_permission has been removed from the settings.
47
+ to configure the permission policy, a security policy has to be implemented.
48
+
49
+ ## 0.14.0 - Released on 2024-09-26
50
+ * Implemement method add_template_search_path in the configurator
51
+ * Add a route_prefix in the configurator for configurator.include
52
+
53
+ ## 0.13.0 - Released on 2024-09-25
54
+ * Add a way to handle api
55
+ * Add a @view_config decorator to register route
56
+ * Add a @resource decorator to handle CRUD resource in rest format
57
+ * Add @exception_handler decorator
58
+ * Add i18n support
59
+
60
+ ## 0.12.0 - Released on 2024-09-19
61
+ * Add a way to register API routes and expose api doc
62
+
63
+ ## 0.11.1 - Released on 2024-09-18
64
+ * Update FastAPI version
65
+
66
+ ## 0.11.0 - Released on 2024-09-18
67
+ * Huge documentation update
68
+ * Use sphinx-autodoc2
69
+ * Add documentation for the components.
70
+ * Breaking change in the configurator.
71
+ * get_app has been renamed get_asgi_app
72
+ * a few internals classes moved/renamed.
73
+
74
+ ## 0.10.0 - Released on 2024-08-24
75
+
76
+ * Rename model_result and ModelResult to form_model and FormModel
77
+ * Add an edit method for FormModel
78
+ * Add a Textarea widget and fix Hidden widget
79
+ * Fix rendering of sequence
80
+ * Do not render main form as nested models
81
+ * Add many functional tests for form field generations
82
+
83
+ ## 0.9.7 - Released on 2024-08-21
84
+
85
+ * Add title attribute to icons
86
+
87
+ ## 0.9.6 - Released on 2024-08-18
88
+
89
+ * Add more buttons options for htmx ajax call
90
+ * Fix Option id
91
+
92
+ ## 0.9.5 - Released on 2024-08-17
93
+
94
+ * Use icons to customize collapsible widget for sequence
95
+ * Add parameter for button to avoid send params
96
+
97
+ ## 0.9.4 - Released on 2024-08-16
98
+
99
+ * Don't update browser url while manipulating autoform lists
100
+
101
+ ## 0.9.3 - Released on 2024-08-16
102
+
103
+ * Fix autoform widgets from jinjax migration
104
+
105
+ ## 0.9.2 - Released on 2024-08-13
106
+
107
+ * Add a constants class for global variable in templates
108
+ * Use icons to customize collapsible widget
109
+
110
+ ## 0.9.1 - Released on 2024-08-12
111
+
112
+ * Replace fa icons by hero icons
113
+
114
+ ## 0.9.0 - Released on 2024-08-12
115
+
116
+ * Add fa Icons (extra)
117
+
118
+ ## 0.8.0 - Released on 2024-08-10
119
+
120
+ * Upgrade JinjaX (Template update required, use vue-like syntax now)
121
+
122
+ ## 0.7.3 - Released on 2024-08-10
123
+
124
+ * Add some HTML markup
125
+
126
+ ## 0.7.2 - Released on 2024-08-07
127
+
128
+ * Fix https behind a reverse proxy
129
+
130
+ ## 0.7.1 - Released on 2024-08-04
131
+
132
+ * Add the registry on request for exception handler
133
+
134
+ ## 0.7.0 - Released on 2024-08-04
135
+
136
+ * Rewrite how the registry is handled, now part of the request (request.registry)
137
+ * Update to get hx-confirm and hx-delete on button
138
+
139
+ ## 0.6.1 - Released on 2024-04-27
140
+
141
+ * Display errors on every widget
142
+
143
+ ## 0.6.0 - Released on 2024-04-25
144
+
145
+ * Refactor the pydantic_form to start handling errors in form.
146
+
147
+ ## 0.5.1 - Released on 2024-04-24
148
+
149
+ * Fix minimum dependency version for JinjaX
150
+
151
+ ## 0.5.0 - Released on 2024-04-24
152
+
153
+ * Implement new types for pydantic form: Enum, Set[Literal] and Set[Enum]
154
+
155
+ ## 0.4.1 - Released on 2024-04-20
156
+
157
+ * Add globals to render custom widget with global data
158
+
159
+ ## 0.4.0 - Released on 2024-04-20
160
+
161
+ * Update JinjaX for global template var support
162
+ * Add lots of missing unit tests
163
+ * Add support of more html form element
164
+ * Update deps
165
+
166
+ ## 0.3.1 - Released on 2024-03-29
167
+
168
+ * Update FastAPI
169
+
170
+ ## 0.3.0 - Released on 2024-03-29
171
+
172
+ * Replace jinja2 by JinjaX
173
+
174
+ ## 0.2.3 - Released on 2024-01-29
175
+
176
+ * Add support of relative import in :class:`Configurator.include` method
177
+
178
+ ## 0.2.2 - Released on 2024-01-28
179
+
180
+ * Add another settings for session domain cookie
181
+ * Update test client wrapper and also wrap bs4 tag
182
+ * Fix session cleanup to properly logout
183
+
184
+ ## 0.2.1 - Released on 2024-01-27
185
+
186
+ * Change add_route signature
187
+ * Set the name of the route mandatory and first argument (breaking change)
188
+ * Add a permission argument
189
+ * Add a settings to inject a check_permission handler
190
+
191
+ ## 0.2.0 - Released on 2024-01-24
192
+
193
+ * Add a session wrapper in the test client
194
+ Allows to initialize session data in tests
195
+
196
+ ## 0.1.2 - Released on 2024-01-15
197
+
198
+ * Handle sessions
199
+
200
+ ## 0.1.1 - Released on 2024-01-05
201
+
202
+ * Update fastapi depencency
203
+
204
+ ## 0.1.0 - Released on 2024-01-05
205
+
206
+ * Initial release
fastlife/__init__.py CHANGED
@@ -1,9 +1,14 @@
1
+ from importlib import metadata
2
+
3
+ __version__ = metadata.version("fastlifeweb")
4
+
1
5
  from fastapi import Response
2
6
 
3
7
  from .config import (
4
8
  Configurator,
5
9
  DefaultRegistry,
6
10
  GenericConfigurator,
11
+ GenericRegistry,
7
12
  Settings,
8
13
  configure,
9
14
  resource,
@@ -21,6 +26,7 @@ __all__ = [
21
26
  "GenericConfigurator",
22
27
  "Configurator",
23
28
  "DefaultRegistry",
29
+ "GenericRegistry",
24
30
  "TemplateParams",
25
31
  "Settings",
26
32
  "view_config",
@@ -3,6 +3,7 @@ Template rending based on JinjaX.
3
3
  """
4
4
 
5
5
  import logging
6
+ import textwrap
6
7
  from collections.abc import Mapping, MutableMapping, Sequence
7
8
  from typing import (
8
9
  TYPE_CHECKING,
@@ -16,6 +17,7 @@ from fastlife import Request
16
17
  from fastlife.adapters.jinjax.widget_factory.factory import WidgetFactory
17
18
  from fastlife.request.form import FormModel
18
19
  from fastlife.request.localizer import get_localizer
20
+ from fastlife.templates.inline import InlineTemplate
19
21
 
20
22
  if TYPE_CHECKING:
21
23
  from fastlife.config.settings import Settings # coverage: ignore
@@ -102,8 +104,7 @@ class JinjaxRenderer(AbstractTemplateRenderer):
102
104
  child components without "props drilling".
103
105
  :param params: parameters used to render the template.
104
106
  """
105
- # Jinja template does accept the file extention while rendering the template
106
- # we strip it before rendering.
107
+
107
108
  template = template[: -len(self.settings.jinjax_file_ext) - 1]
108
109
  if globals:
109
110
  self.globals.update(globals)
@@ -116,6 +117,27 @@ class JinjaxRenderer(AbstractTemplateRenderer):
116
117
  template, __globals=self.build_globals(), **params
117
118
  )
118
119
 
120
+ def render_inline(self, template: InlineTemplate) -> str:
121
+ """
122
+ Render the JinjaX component with the given parameter.
123
+
124
+ :param template: the template to render
125
+ :param globals: parameters that will be used by the JinjaX component and all its
126
+ child components without "props drilling".
127
+ :param params: parameters used to render the template.
128
+ """
129
+ params = template.model_dump()
130
+ src = (
131
+ f"{{# def {', '.join(params.keys())} #}}\n"
132
+ f"{textwrap.dedent(template.template)}"
133
+ )
134
+ return self.catalog.render(
135
+ template.__class__.__qualname__,
136
+ __source=src,
137
+ __globals=self.build_globals(),
138
+ **params,
139
+ )
140
+
119
141
  def pydantic_form(
120
142
  self, model: FormModel[Any], *, token: str | None = None
121
143
  ) -> Markup:
@@ -1,7 +1,7 @@
1
1
  """Configure fastlife app for dependency injection."""
2
2
 
3
3
  from .configurator import Configurator, GenericConfigurator, configure
4
- from .registry import DefaultRegistry
4
+ from .registry import DefaultRegistry, GenericRegistry
5
5
  from .resources import resource, resource_view
6
6
  from .settings import Settings
7
7
  from .views import view_config
@@ -13,6 +13,7 @@ __all__ = [
13
13
  "view_config",
14
14
  "resource",
15
15
  "resource_view",
16
+ "GenericRegistry",
16
17
  "DefaultRegistry",
17
18
  "Settings",
18
19
  ]
@@ -466,7 +466,11 @@ class GenericConfigurator(Generic[TRegistry]):
466
466
  :param path: path of the route, use `{curly_brace}` to inject FastAPI Path
467
467
  parameters.
468
468
  :param endpoint: the function that will reveive the request.
469
- :param permission: a permission to validate by the security policy.
469
+ :param template: the template rendered by the
470
+ {class}`fastlife.service.templates.AbstractTemplateRenderer`.
471
+ :param permission: a permission to validate by the
472
+ {class}`Security Policy <fastlife.security.policy.AbstractSecurityPolicy>`.
473
+ :param status_code: customize response status code.
470
474
  :param methods: restrict route to a list of http methods.
471
475
  :return: the configurator.
472
476
  """
@@ -1,5 +1,5 @@
1
1
  from collections.abc import Mapping
2
- from typing import TYPE_CHECKING, TypeVar
2
+ from typing import TYPE_CHECKING, Generic, TypeVar
3
3
 
4
4
  from fastlife.services.locale_negociator import LocaleNegociator, default_negociator
5
5
  from fastlife.services.translations import LocalizerFactory
@@ -11,10 +11,15 @@ if TYPE_CHECKING:
11
11
 
12
12
  from .settings import Settings
13
13
 
14
+ TSettings = TypeVar("TSettings", bound=Settings, covariant=True)
15
+ """
16
+ A TypeVar used to override the DefaultRegistry to add more helpers in the registry.
17
+ """
18
+
14
19
 
15
- class DefaultRegistry:
20
+ class GenericRegistry(Generic[TSettings]):
16
21
  """
17
- The application registry got fastlife dependency injection.
22
+ Application registry for fastlife dependency injection.
18
23
  It is initialized by the configurator and accessed by the `fastlife.Registry`.
19
24
  """
20
25
 
@@ -36,6 +41,12 @@ class DefaultRegistry:
36
41
  raise RuntimeError(f"No renderer registered for template {template}")
37
42
 
38
43
 
44
+ DefaultRegistry = GenericRegistry[Settings]
45
+ """
46
+ The default registry until you need to inject more component in the registry.
47
+ """
48
+
49
+
39
50
  TRegistry = TypeVar("TRegistry", bound=DefaultRegistry, covariant=True)
40
51
  """
41
52
  A TypeVar used to override the DefaultRegistry to add more helpers in the registry.
fastlife/config/views.py CHANGED
@@ -36,14 +36,20 @@ def view_config(
36
36
  methods: list[str] | None = None,
37
37
  ) -> Callable[..., Any]:
38
38
  """
39
- A decorator function to add a view in the app.
39
+ A decorator function to register a view in the
40
+ {class}`Configurator <fastlife.config.configurator.GenericConfigurator>`
41
+ while scaning a module using {func}`include
42
+ <fastlife.config.configurator.GenericConfigurator.include>`.
40
43
 
41
44
  :param name: name of the route, used to build route from the helper
42
45
  {meth}`fastlife.request.request.Request.url_for` in order to create links.
43
46
  :param path: path of the route, use `{curly_brace}` to inject FastAPI Path
44
47
  parameters.
48
+ :param template: the template rendered by the
49
+ {class}`fastlife.services.templates.AbstractTemplateRenderer`.
45
50
  :param permission: a permission to validate by the
46
- {attr}`fastlife.config.settings.Settings.check_permission` function.
51
+ {class}`Security Policy <fastlife.security.policy.AbstractSecurityPolicy>`.
52
+ :param status_code: customize response status code.
47
53
  :param methods: restrict route to a list of http methods.
48
54
 
49
55
  :return: the configuration callback.
@@ -63,6 +63,7 @@ class HasPermission(int, metaclass=BoolMeta):
63
63
  where authenticated user are not redirected. they have an error message,
64
64
  or the frontend may use the information to adapt its interface.
65
65
  """
66
+
66
67
  kind: Literal["allowed", "unauthenticated", "denied"]
67
68
  reason: str
68
69
 
@@ -1,12 +1,11 @@
1
1
  """
2
2
  Base class to of the template renderer.
3
3
 
4
- Fastlife comes with {class}`fastlife.templating.renderer.jinjax.JinjaxEngine`,
5
- the rendering engine, it can be overriden from the setting
6
- :attr:`fastlife.config.settings.Settings.template_renderer_class`.
7
-
8
- In that case, those base classes have to be implemented.
4
+ Fastlife comes with {class}`fastlife.adapters.jinjax.renderer.JinjaxEngine`,
5
+ the rendering engine.
9
6
 
7
+ More template engine can be registered using the configurator method
8
+ {meth}`add_renderer <fastlife.config.configurator.GenericConfigurator.add_renderer>`
10
9
  """
11
10
 
12
11
  import abc
@@ -15,6 +14,7 @@ from typing import Any
15
14
 
16
15
  from fastlife import Request, Response
17
16
  from fastlife.security.csrf import create_csrf_token
17
+ from fastlife.templates.inline import InlineTemplate
18
18
 
19
19
  TemplateParams = Mapping[str, Any]
20
20
 
@@ -43,7 +43,7 @@ class AbstractTemplateRenderer(abc.ABC):
43
43
  status_code: int = 200,
44
44
  content_type: str = "text/html",
45
45
  globals: Mapping[str, Any] | None = None,
46
- params: TemplateParams,
46
+ params: TemplateParams | InlineTemplate,
47
47
  _create_csrf_token: Callable[..., str] = create_csrf_token,
48
48
  ) -> Response:
49
49
  """
@@ -54,7 +54,10 @@ class AbstractTemplateRenderer(abc.ABC):
54
54
  request.scope[reg.settings.csrf_token_name] = (
55
55
  request.cookies.get(reg.settings.csrf_token_name) or _create_csrf_token()
56
56
  )
57
- data = self.render_template(template, **params)
57
+ if isinstance(params, InlineTemplate):
58
+ data = self.render_inline(params)
59
+ else:
60
+ data = self.render_template(template, **params)
58
61
  resp = Response(
59
62
  data, status_code=status_code, headers={"Content-Type": content_type}
60
63
  )
@@ -94,6 +97,15 @@ class AbstractTemplateRenderer(abc.ABC):
94
97
  :return: The template rendering result.
95
98
  """
96
99
 
100
+ @abc.abstractmethod
101
+ def render_inline(self, template: InlineTemplate) -> str:
102
+ """
103
+ Render an inline template.
104
+
105
+ :param template: the template to render.
106
+ :return: The template rendering result.
107
+ """
108
+
97
109
 
98
110
  class AbstractTemplateRendererFactory(abc.ABC):
99
111
  """
@@ -1,18 +1,21 @@
1
1
  import pathlib
2
- from collections.abc import Iterator
2
+ from collections import defaultdict
3
+ from collections.abc import Callable, Iterator
4
+ from gettext import GNUTranslations
5
+ from io import BufferedReader
3
6
  from typing import TYPE_CHECKING
4
7
 
5
- from babel.support import NullTranslations, Translations
6
-
7
8
  from fastlife.shared_utils.resolver import resolve_path
8
9
 
9
10
  if TYPE_CHECKING:
10
11
  from fastlife import Request # coverage: ignore
11
12
 
12
- locale_name = str
13
+ LocaleName = str
14
+ Domain = str
15
+ CONTEXT_ENCODING = "%s\x04%s"
13
16
 
14
17
 
15
- def find_mo_files(root_path: str) -> Iterator[tuple[str, str, pathlib.Path]]:
18
+ def find_mo_files(root_path: str) -> Iterator[tuple[LocaleName, Domain, pathlib.Path]]:
16
19
  """
17
20
  Find .mo files in a locales directory.
18
21
 
@@ -30,30 +33,54 @@ def find_mo_files(root_path: str) -> Iterator[tuple[str, str, pathlib.Path]]:
30
33
  yield locale_dir.name, mo_file.stem, mo_file
31
34
 
32
35
 
36
+ def _default_plural(n: int) -> int:
37
+ return int(n != 1) # germanic plural by default
38
+
39
+
40
+ class MergedTranslations(GNUTranslations):
41
+ _catalog: dict[str, str]
42
+
43
+ def __init__(self) -> None:
44
+ super().__init__()
45
+ self._catalog = {}
46
+ self.plural: Callable[[int], int] = _default_plural
47
+
48
+ def merge(self, other: GNUTranslations) -> None:
49
+ if hasattr(other, "_catalog"):
50
+ self._catalog.update(other._catalog) # type: ignore
51
+ if hasattr(other, "plural"):
52
+ self.plural = other.plural # type: ignore
53
+
54
+
33
55
  class Localizer:
34
- def __init__(
35
- self, request: "Request", translations: Translations | NullTranslations
36
- ) -> None:
37
- self.locale_name = request.locale_name
38
- self.translations = translations
56
+ def __init__(self) -> None:
57
+ self.translations: dict[Domain, MergedTranslations] = defaultdict(
58
+ MergedTranslations
59
+ )
60
+ self.global_translations = MergedTranslations()
61
+
62
+ def register(self, domain: str, file: BufferedReader) -> None:
63
+ trans = GNUTranslations(file)
64
+ self.translations[domain].merge(trans)
65
+ self.global_translations.merge(trans)
39
66
 
40
67
  def gettext(self, message: str, mapping: dict[str, str] | None = None) -> str:
41
- ret = self.translations.gettext(message)
42
- if mapping is not None:
68
+ ret = self.global_translations.gettext(message)
69
+ if mapping:
43
70
  ret = ret.format(**mapping)
44
71
  return ret
45
72
 
46
73
  def ngettext(
47
74
  self, singular: str, plural: str, n: int, mapping: dict[str, str] | None = None
48
75
  ) -> str:
49
- ret = self.translations.ngettext(singular, plural, n)
76
+ ret = self.global_translations.ngettext(singular, plural, n)
50
77
  mapping_num = {"num": n, **(mapping or {})}
51
78
  return ret.format(**mapping_num)
52
79
 
53
80
  def dgettext(
54
81
  self, domain: str, message: str, mapping: dict[str, str] | None = None
55
82
  ) -> str:
56
- ret = self.translations.dgettext(domain, message)
83
+ ret = self.translations[domain].gettext(message)
57
84
  if mapping:
58
85
  ret = ret.format(**mapping)
59
86
  return ret
@@ -66,14 +93,14 @@ class Localizer:
66
93
  n: int,
67
94
  mapping: dict[str, str] | None = None,
68
95
  ) -> str:
69
- ret = self.translations.dngettext(domain, singular, plural, n)
96
+ ret = self.translations[domain].ngettext(singular, plural, n)
70
97
  mapping_num = {"num": n, **(mapping or {})}
71
98
  return ret.format(**mapping_num)
72
99
 
73
100
  def pgettext(
74
101
  self, context: str, message: str, mapping: dict[str, str] | None = None
75
102
  ) -> str:
76
- ret = str(self.translations.pgettext(context, message))
103
+ ret = self.global_translations.pgettext(context, message)
77
104
  if mapping:
78
105
  ret = ret.format(**mapping)
79
106
  return ret
@@ -85,7 +112,7 @@ class Localizer:
85
112
  message: str,
86
113
  mapping: dict[str, str] | None = None,
87
114
  ) -> str:
88
- ret = str(self.translations.dpgettext(domain, context, message))
115
+ ret = self.translations[domain].pgettext(context, message)
89
116
  if mapping:
90
117
  ret = ret.format(**mapping)
91
118
  return ret
@@ -99,18 +126,33 @@ class Localizer:
99
126
  n: int,
100
127
  mapping: dict[str, str] | None = None,
101
128
  ) -> str:
102
- ret = self.translations.dnpgettext(domain, context, singular, plural, n)
129
+ ret = self.translations[domain].npgettext(context, singular, plural, n)
103
130
  mapping_num = {"num": n, **(mapping or {})}
104
131
  return ret.format(**mapping_num)
105
132
 
106
133
 
134
+ class TranslationDictionary:
135
+ def __init__(self) -> None:
136
+ self.translations: dict[LocaleName, Localizer] = defaultdict(Localizer)
137
+
138
+ def load(self, root_path: str) -> None:
139
+ for locale_name, domain, file_ in find_mo_files(root_path):
140
+ with file_.open("rb") as stream:
141
+ self.translations[locale_name].register(domain, stream)
142
+
143
+ def get(self, locale_name: LocaleName) -> Localizer:
144
+ return self.translations[locale_name]
145
+
146
+ def __contains__(self, other: LocaleName) -> bool:
147
+ return other in self.translations
148
+
149
+
107
150
  class LocalizerFactory:
108
151
  """Initialize the proper translation context per request."""
109
152
 
110
- _translations: dict[locale_name, Translations]
111
-
112
153
  def __init__(self) -> None:
113
- self._translations = {}
154
+ self._translations = TranslationDictionary()
155
+ self.null_localizer = Localizer()
114
156
 
115
157
  def load(self, path: str) -> None:
116
158
  """
@@ -118,21 +160,10 @@ class LocalizerFactory:
118
160
  :param path: a python module and the locales dir separated by a `:`
119
161
  """
120
162
  root_path = resolve_path(path)
121
- for locale_name, domain, file_ in find_mo_files(root_path):
122
- with file_.open("rb") as f:
123
- t = Translations(f, domain)
124
- if locale_name not in self._translations:
125
- self._translations[locale_name] = Translations()
126
- self._translations[locale_name].add(t)
127
- self._translations[locale_name].merge(t)
163
+ self._translations.load(root_path)
128
164
 
129
165
  def __call__(self, request: "Request") -> Localizer:
130
166
  """Create the translation context for the given request."""
131
- trans: Translations | NullTranslations | None = self._translations.get(
132
- request.locale_name
133
- )
134
- if not trans:
135
- trans = self._translations.get(request.registry.settings.default_locale)
136
- if not trans:
137
- trans = NullTranslations()
138
- return Localizer(request, trans)
167
+ if request.locale_name not in self._translations:
168
+ return self.null_localizer
169
+ return self._translations.get(request.locale_name)
@@ -3,8 +3,10 @@ Utilities for rendering HTML templates for page and components as FastAPI depend
3
3
  """
4
4
 
5
5
  from .binding import Template, template
6
+ from .inline import InlineTemplate
6
7
 
7
8
  __all__ = [
8
9
  "Template",
9
10
  "template",
11
+ "InlineTemplate",
10
12
  ]
@@ -0,0 +1,22 @@
1
+ """Inline templates."""
2
+
3
+ from typing import ClassVar
4
+
5
+ from pydantic import BaseModel
6
+ from pydantic.config import ConfigDict
7
+
8
+
9
+ class InlineTemplate(BaseModel):
10
+ """
11
+ Inline templates are used to encourage the location of behavior and the view typing.
12
+
13
+ Pages produce templates that are not reusable and don't need to be reusable
14
+ in there essence, they don't need to be in a component library.
15
+ They use a component lirary to stay small but contains a view logic
16
+ tighly coupled with the view and its code can stay in the same module of that view.
17
+ """
18
+
19
+ model_config = ConfigDict(arbitrary_types_allowed=True)
20
+
21
+ template: ClassVar[str]
22
+ """The template string to render."""
@@ -1,43 +1,44 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fastlifeweb
3
- Version: 0.18.0
3
+ Version: 0.20.0
4
4
  Summary: High-level web framework
5
- Home-page: https://github.com/mardiros/fastlife
6
- License: BSD-derived
7
- Author: Guillaume Gauvrit
8
- Author-email: guillaume@gauvr.it
9
- Requires-Python: >=3.11,<4.0
5
+ Author-Email: Guillaume Gauvrit <guillaume@gauvr.it>
6
+ License: MIT License
10
7
  Classifier: Development Status :: 4 - Beta
11
8
  Classifier: Framework :: AsyncIO
12
9
  Classifier: Intended Audience :: Developers
13
- Classifier: License :: OSI Approved :: BSD License
14
- Classifier: License :: Other/Proprietary License
15
- Classifier: Programming Language :: Python :: 3
16
- Classifier: Programming Language :: Python :: 3.11
17
- Classifier: Programming Language :: Python :: 3.12
18
- Classifier: Topic :: Internet :: WWW/HTTP
10
+ Classifier: License :: OSI Approved :: MIT License
19
11
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
- Provides-Extra: doc
12
+ Classifier: Topic :: Internet :: WWW/HTTP
13
+ Project-URL: Homepage, https://mardiros.github.io/fastlife
14
+ Project-URL: Documentation, https://mardiros.github.io/fastlife
15
+ Project-URL: Repository, https://github.com/mardiros/fastlife.git
16
+ Project-URL: Issues, https://github.com/mardiros/fastlife/issues
17
+ Project-URL: Changelog, https://mardiros.github.io/fastlife/user/changelog.html
18
+ Requires-Python: >=3.11
19
+ Requires-Dist: fastapi[standard]<1,>=0.115.0
20
+ Requires-Dist: itsdangerous<3,>=2.1.2
21
+ Requires-Dist: jinjax<0.45,>=0.44
22
+ Requires-Dist: markupsafe<3,>=2.1.3
23
+ Requires-Dist: multidict<7,>=6.0.5
24
+ Requires-Dist: pydantic<3,>=2.5.3
25
+ Requires-Dist: pydantic-settings<3,>=2.0.3
26
+ Requires-Dist: python-multipart<1,>=0.0.9
27
+ Requires-Dist: venusian<4,>=3.0.0
21
28
  Provides-Extra: testing
22
- Requires-Dist: babel (>=2.16.0,<3.0.0)
23
- Requires-Dist: beautifulsoup4 (>=4.12.2,<5.0.0) ; extra == "testing"
24
- Requires-Dist: fastapi[standard] (>=0.115.0,<0.116.0)
25
- Requires-Dist: itsdangerous (>=2.1.2,<3.0.0)
26
- Requires-Dist: jinjax (>=0.44,<0.45)
27
- Requires-Dist: markupsafe (>=2.1.3,<3.0.0)
28
- Requires-Dist: multidict (>=6.0.5,<7.0.0)
29
- Requires-Dist: pydantic (>=2.5.3,<3.0.0)
30
- Requires-Dist: pydantic-settings (>=2.0.3,<3.0.0)
31
- Requires-Dist: python-multipart (>=0.0.9,<0.0.10)
32
- Requires-Dist: sphinx (>=7.0.1,<8.0.0) ; extra == "doc"
33
- Requires-Dist: venusian (>=3.0.0,<4.0.0)
34
- Project-URL: Repository, https://github.com/mardiros/fastlife
29
+ Requires-Dist: beautifulsoup4; extra == "testing"
30
+ Provides-Extra: docs
31
+ Requires-Dist: furo>=2024.5.6; extra == "docs"
32
+ Requires-Dist: linkify-it-py<3,>=2.0.3; extra == "docs"
33
+ Requires-Dist: myst-parser<5,>=4.0.0; extra == "docs"
34
+ Requires-Dist: sphinx<8,>=7.0.1; extra == "docs"
35
+ Requires-Dist: sphinx-autodoc2<1,>=0.5.0; extra == "docs"
35
36
  Description-Content-Type: text/markdown
36
37
 
37
38
  # Fastlife
38
39
 
39
- [![Documentation](https://github.com/mardiros/fastlife/actions/workflows/gh-pages.yml/badge.svg)](https://mardiros.github.io/fastlife/)
40
- [![Continuous Integration](https://github.com/mardiros/fastlife/actions/workflows/main.yml/badge.svg)](https://github.com/mardiros/fastlife/actions/workflows/main.yml)
40
+ [![Documentation](https://github.com/mardiros/fastlife/actions/workflows/publish-doc.yml/badge.svg)](https://mardiros.github.io/fastlife/)
41
+ [![Continuous Integration](https://github.com/mardiros/fastlife/actions/workflows/tests.yml/badge.svg)](https://github.com/mardiros/fastlife/actions/workflows/tests.yml)
41
42
  [![Coverage Report](https://codecov.io/gh/mardiros/fastlife/graph/badge.svg?token=DTpi73d7mf)](https://codecov.io/gh/mardiros/fastlife)
42
43
  [![Maintainability](https://api.codeclimate.com/v1/badges/94d107797b15b5e8843e/maintainability)](https://codeclimate.com/github/mardiros/fastlife/maintainability)
43
44
 
@@ -85,4 +86,3 @@ The package is available on pypi with the name fastlifeweb.
85
86
  ```bash
86
87
  pip install fastlifeweb
87
88
  ```
88
-
@@ -1,4 +1,5 @@
1
- fastlife/__init__.py,sha256=fokakuhI0fdAjHP5w6GWi-YfCx7iTnrVzjSyZ11Cdgg,676
1
+ CHANGELOG.md,sha256=O4fJAjGiCGsX1L56Rdtl_4KbruwAsqsxihHgtd5FnDc,6032
2
+ fastlife/__init__.py,sha256=l8SVz6RNy8QJNKFwUx9OHt-V21r9yeKK8pLuUY1u4ps,799
2
3
  fastlife/adapters/__init__.py,sha256=WYjEN8gp4r7LCHqmIO5VzzvsT8QGRE3w4G47UwYDtAo,94
3
4
  fastlife/adapters/jinjax/__init__.py,sha256=4JRAUwFGpTxYtRlg5sU79AahxyAiRMhllRFHoI-dnug,117
4
5
  fastlife/adapters/jinjax/jinjax_ext/__init__.py,sha256=z6NWvHzTNEq6bVO4iJoTR6-y4A6UtS_VuSMV_tff1jY,49
@@ -6,7 +7,7 @@ fastlife/adapters/jinjax/jinjax_ext/docstring.py,sha256=Zlx0oSxsRU9vQvGoyScXf9uB
6
7
  fastlife/adapters/jinjax/jinjax_ext/inspectable_catalog.py,sha256=KHOYTT6UNA41QwyLNQVoG4trOVXYdChlRmqq_G1pv1s,2987
7
8
  fastlife/adapters/jinjax/jinjax_ext/inspectable_component.py,sha256=Cz6PrRhO3lXUI9baxosOr6MtZFjuOki9YTEWMkfbbR0,2909
8
9
  fastlife/adapters/jinjax/jinjax_ext/jinjax_doc.py,sha256=uPcYiUTrliR2lLpRuQUrirmFzTEVRTwo1mUZE4Z5onc,10225
9
- fastlife/adapters/jinjax/renderer.py,sha256=Ii6qhbQEQmpbvJAafRwn88ePuA2ySB_nJ11BloCjltg,6160
10
+ fastlife/adapters/jinjax/renderer.py,sha256=6HjngkDNWUTlR_ST3v_WDu8KD4PJnniK74eQl_RXdzU,6866
10
11
  fastlife/adapters/jinjax/widget_factory/__init__.py,sha256=Dy_2xr_YDAyEF9WtNpjV-aYaehRO1iKEIHVFdfFeszw,59
11
12
  fastlife/adapters/jinjax/widget_factory/base.py,sha256=TLEpYdekR4AeHhIie_DICc_oSvQaUaL8GlatqNiRewg,1046
12
13
  fastlife/adapters/jinjax/widget_factory/bool_builder.py,sha256=2-Hv5w4hfBfGWGetb00I8Lm1FDAputH2MNt3tCx-RbA,1280
@@ -1682,14 +1683,14 @@ fastlife/components/pydantic_form/Text.jinja,sha256=2f_3Q32GySHTLFt-YO8gEJNCY-3X
1682
1683
  fastlife/components/pydantic_form/Textarea.jinja,sha256=NzfCi5agRUSVcb5RXw0QamM8P1lZ-CdNI6P30zb2948,1155
1683
1684
  fastlife/components/pydantic_form/Union.jinja,sha256=czTska54z9KCZKu-FaycLmOvtH6y6CGUFQ8DHnkjrJk,1461
1684
1685
  fastlife/components/pydantic_form/Widget.jinja,sha256=EXskDqt22D5grpGVwlZA3ndve2Wr_6yQH4qVE9c31Og,397
1685
- fastlife/config/__init__.py,sha256=ThosRIPZ_fpD0exZu-kUC_f8ZNa5KyDlleWMmEHkjEo,448
1686
- fastlife/config/configurator.py,sha256=Elf6S9syFDnpX6TWCcF7lZhwXI4W573htuaeWzGj8oI,22390
1686
+ fastlife/config/__init__.py,sha256=R0HXh0rt6Aecf5kJmXvhH-6QrQb1SyuI83bOU8T80fc,488
1687
+ fastlife/config/configurator.py,sha256=sVecDJbjeRQRjULjbQH1JCeBOqvS6DUeQGJ5Nh2HQlw,22649
1687
1688
  fastlife/config/exceptions.py,sha256=kH2-akbzGeODlY_1bUhbzDKqBFrpOoqnVom0WPm0IGg,1237
1688
1689
  fastlife/config/openapiextra.py,sha256=rYoerrn9sni2XwnO3gIWqaz7M0aDZPhVLjzqhDxue0o,514
1689
- fastlife/config/registry.py,sha256=geny-liWxciPpHjmeFba9zLuXNWbz-R99VtzPS0whg0,1439
1690
+ fastlife/config/registry.py,sha256=zP_LPvwTcUZdrrsCnRaUfJzeYq7M_eVdRWlN0oO52RE,1754
1690
1691
  fastlife/config/resources.py,sha256=Wu3vVr7XD18Gf4-MYYCxAAnuRmsAJmpllonts_BVGdQ,8593
1691
1692
  fastlife/config/settings.py,sha256=t9goMfnc_oWrS_c3vgivIe0w6N2Byohcfol2CAnLiJs,3892
1692
- fastlife/config/views.py,sha256=V-P53GSnvqEPzkvEWNuI4ofcdbFur2Dl-s6BeKXObwI,2086
1693
+ fastlife/config/views.py,sha256=C6Ot3mxTyN5isVsIe5b9x-VzD2q76pJQ8xY2aPWdMZM,2460
1693
1694
  fastlife/middlewares/__init__.py,sha256=C3DUOzR5EhlAv5Zq7h-Abyvkd7bUsJohTRSB2wpRYQE,220
1694
1695
  fastlife/middlewares/base.py,sha256=9OYqByRuVoIrLt353NOedPQTLdr7LSmxhb2BZcp20qk,638
1695
1696
  fastlife/middlewares/reverse_proxy/__init__.py,sha256=g1SoVDmenKzpAAPYHTEsWgdBByOxtLg9fGx6RV3i0ok,846
@@ -1708,18 +1709,19 @@ fastlife/routing/route.py,sha256=vqjfMsHAVO0l2B8fuB8t19CKMtE7WoBkG4kvi4lUonM,144
1708
1709
  fastlife/routing/router.py,sha256=ho9TvTkX2iUW6GEh99FgclZVFKkCCCxYG4pPHeUtGn8,482
1709
1710
  fastlife/security/__init__.py,sha256=QYDcJ3oXQzqXQxoDD_6biGAtercFrtePttoifiL1j34,25
1710
1711
  fastlife/security/csrf.py,sha256=PIKG83LPqKz4kDALnZxIyPdYVwbNqsIryi7JPqRPQag,2168
1711
- fastlife/security/policy.py,sha256=t7u6PRysvhJNgU-rWsANpXQvNrTTlHizxWaV7LJ4wfo,5578
1712
+ fastlife/security/policy.py,sha256=4WHu8xhR7tAdXUHpHY1kdVIdTOFsUl92R8IsIRyYMSU,5579
1712
1713
  fastlife/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1713
1714
  fastlife/services/locale_negociator.py,sha256=Np2O8s7xnYTpf5eCG7LvcfFJ2LV7p_k86NNrU9Lju88,846
1714
1715
  fastlife/services/policy.py,sha256=RfYGPjfEAAoHECUnZVLPZgN0iRanu8UKQSky6oAz81o,1687
1715
- fastlife/services/templates.py,sha256=XHl6AqyAWNNeSJpaal5EZCptx9dYusTDNQbZwjofAPc,3857
1716
- fastlife/services/translations.py,sha256=Fu93zSc3ajNVFfAqw_G0nBV9bitss9Xy-he9lSHx0V8,4387
1716
+ fastlife/services/templates.py,sha256=xSoVofpKXrZc1fDLAJyDDceJrKZaZRL1K8wFp2rGsE0,4272
1717
+ fastlife/services/translations.py,sha256=Akazow9YTb6N2IDdMZOAMef-grbAC7fM0nA3hw-mHt4,5315
1717
1718
  fastlife/shared_utils/__init__.py,sha256=i66ytuf-Ezo7jSiNQHIsBMVIcB-tDX0tg28-pUOlhzE,26
1718
1719
  fastlife/shared_utils/infer.py,sha256=3G_u6q2aWzeiVlAyGaWIlnAcz90m4bFNwpPYd5JIqfE,723
1719
1720
  fastlife/shared_utils/resolver.py,sha256=Wb9cO2MWavpti63hju15xmwFMgaD5DsQaxikRpB39E8,3713
1720
- fastlife/templates/__init__.py,sha256=QrP_5UAOgxqC-jOu5tcjd-l6GOYrS4dka6vmWMxWqfo,184
1721
+ fastlife/templates/__init__.py,sha256=yB6Zpz-jsFytLAz4Z6Nmuvr7Z8kbCXSdgw6kMpggTxk,241
1721
1722
  fastlife/templates/binding.py,sha256=0pE2btOwLf4xOEgBXVOyz_dIX9tBCYCaJ7RhZI3knbs,1464
1722
1723
  fastlife/templates/constants.py,sha256=MGdUjkF9hsPMN8rOS49eWbAApcb8FL-FAeFvJU8k90M,8387
1724
+ fastlife/templates/inline.py,sha256=wrZDAtd7zXcACHl2LVz0HqDT1VSPsiVy4srt3JSDiu4,704
1723
1725
  fastlife/testing/__init__.py,sha256=VpxkS3Zp3t_hH8dBiLaGFGhsvt511dhBS_8fMoFXdmU,99
1724
1726
  fastlife/testing/dom.py,sha256=dVzDoZokn-ii681UaEwAr-khM5KE-CHgXSSLSo24oH0,4489
1725
1727
  fastlife/testing/form.py,sha256=ST0xNCoUqz_oD92cWHzQ6CbJ5hFopvu_NNKpOfiuYWY,7874
@@ -1727,7 +1729,9 @@ fastlife/testing/session.py,sha256=LEFFbiR67_x_g-ioudkY0C7PycHdbDfaIaoo_G7GXQ8,2
1727
1729
  fastlife/testing/testclient.py,sha256=WmUnGkDPuSd4dKzTiXWyHWlJ31zBbySvMH9m8p0acg8,6741
1728
1730
  fastlife/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1729
1731
  fastlife/views/pydantic_form.py,sha256=4dv37JORLpvkgCgMGZfUN_qy7wme040GLZAzOTFqdnU,1367
1730
- fastlifeweb-0.18.0.dist-info/LICENSE,sha256=F75xSseSKMwqzFj8rswYU6NWS3VoWOc_gY3fJYf9_LI,1504
1731
- fastlifeweb-0.18.0.dist-info/METADATA,sha256=u09i6bAle2qtATT2ViL5zvi7o3BpV_TN9U4NnLJTcmQ,3556
1732
- fastlifeweb-0.18.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
1733
- fastlifeweb-0.18.0.dist-info/RECORD,,
1732
+ fastlifeweb-0.20.0.dist-info/METADATA,sha256=CRTUb1xqKtSTjd5lhZ-YRY9AAJZHjJW14vMvFJngjmk,3663
1733
+ fastlifeweb-0.20.0.dist-info/WHEEL,sha256=thaaA2w1JzcGC48WYufAs8nrYZjJm8LqNfnXFOFyCC4,90
1734
+ fastlifeweb-0.20.0.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
1735
+ fastlifeweb-0.20.0.dist-info/licenses/LICENSE,sha256=NlRX9Z-dcv8X1VFW9odlIQBbgNN9pcO94XzvKp2R16o,1075
1736
+ tailwind.config.js,sha256=y_xTuRmjIdrfyo92PoUW0wjFTnkO27xepwfirkaCFno,1351
1737
+ fastlifeweb-0.20.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.9.0
2
+ Generator: pdm-backend (2.4.3)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -0,0 +1,4 @@
1
+ [console_scripts]
2
+
3
+ [gui_scripts]
4
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024, Guillaume Gauvrit
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.
tailwind.config.js ADDED
@@ -0,0 +1,57 @@
1
+ /** @type {import('tailwindcss').Config} */
2
+ module.exports = {
3
+ darkMode: "class",
4
+ content: [
5
+ "./src/fastlife/templates/constants.py",
6
+ "./src/fastlife/components/*.jinja",
7
+ "./src/fastlife/components/**/*.jinja",
8
+ "./tests/fastlife_app/templates/*.jinja",
9
+ "./tests/fastlife_app/templates/**/*.jinja",
10
+ ],
11
+ theme: {
12
+ extend: {
13
+ colors: {
14
+ primary: {
15
+ 50: "#f0f9ff",
16
+ 100: "#e0f2fe",
17
+ 200: "#bae6fd",
18
+ 300: "#7dd3fc",
19
+ 400: "#38bdf8",
20
+ 500: "#0ea5e9",
21
+ 600: "#0284c7",
22
+ 700: "#0369a1",
23
+ 800: "#075985",
24
+ 900: "#0c4a6e",
25
+ 950: "#082f49",
26
+ },
27
+ danger: {
28
+ 50: "#fef2f2",
29
+ 100: "#fee2e2",
30
+ 200: "#fecaca",
31
+ 300: "#fca5a5",
32
+ 400: "#f87171",
33
+ 500: "#ef4444",
34
+ 600: "#dc2626",
35
+ 700: "#b91c1c",
36
+ 800: "#991b1b",
37
+ 900: "#7f1d1d",
38
+ 950: "#450a0a",
39
+ },
40
+ neutral: {
41
+ 50: "#fafaf9",
42
+ 100: "#f5f5f4",
43
+ 200: "#e7e5e4",
44
+ 300: "#d6d3d1",
45
+ 400: "#a8a29e",
46
+ 500: "#78716c",
47
+ 600: "#57534e",
48
+ 700: "#44403c",
49
+ 800: "#292524",
50
+ 900: "#1c1917",
51
+ 950: "#0c0a09",
52
+ },
53
+ },
54
+ },
55
+ },
56
+ plugins: [],
57
+ };
@@ -1,28 +0,0 @@
1
- BSD 3-Clause License
2
-
3
- Copyright (c) 2024, Guillaume Gauvrit
4
-
5
- Redistribution and use in source and binary forms, with or without
6
- modification, are permitted provided that the following conditions are met:
7
-
8
- 1. Redistributions of source code must retain the above copyright notice, this
9
- list of conditions and the following disclaimer.
10
-
11
- 2. Redistributions in binary form must reproduce the above copyright notice,
12
- this list of conditions and the following disclaimer in the documentation
13
- and/or other materials provided with the distribution.
14
-
15
- 3. Neither the name of the copyright holder nor the names of its
16
- contributors may be used to endorse or promote products derived from
17
- this software without specific prior written permission.
18
-
19
- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
- AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
- DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
- SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
- CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
- OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
- OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.