fastlifeweb 0.23.0__py3-none-any.whl → 0.24.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 CHANGED
@@ -1,3 +1,10 @@
1
+ ## 0.24.0 - Released on 2025-01-17
2
+ * Add a way to hook the lifespan to the app
3
+
4
+ ## 0.23.1 - Released on 2025-01-14
5
+ * Fix typing issue
6
+ * Update docs
7
+
1
8
  ## 0.23.0 - Released on 2024-12-04
2
9
  * Update Request type.
3
10
  * Breaking changes: Request[TUser, TRegistry] -> Request[TRegistry, TIdentity, TClaimedIdentity].
fastlife/__init__.py CHANGED
@@ -48,6 +48,7 @@ from .service.security_policy import (
48
48
  AbstractSecurityPolicy,
49
49
  InsecurePolicy,
50
50
  )
51
+ from .service.translations import TranslatableStringFactory
51
52
  from .settings import Settings
52
53
 
53
54
  __all__ = [
@@ -97,4 +98,5 @@ __all__ = [
97
98
  "JinjaXTemplate",
98
99
  # i18n
99
100
  "Localizer",
101
+ "TranslatableStringFactory",
100
102
  ]
@@ -8,7 +8,7 @@ from fastlife.service.translations import Localizer as RequestLocalizer
8
8
 
9
9
  def get_localizer(request: Request) -> RequestLocalizer:
10
10
  """Return the localizer for the given request."""
11
- return request.registry.localizer(request)
11
+ return request.registry.localizer(request.locale_name)
12
12
 
13
13
 
14
14
  Localizer = Annotated[RequestLocalizer, Depends(get_localizer)]
@@ -191,6 +191,7 @@ class GenericConfigurator(Generic[TRegistry]):
191
191
  dependencies=[Depends(check_csrf())],
192
192
  docs_url=self.api_swagger_ui_url,
193
193
  redoc_url=self.api_redoc_url,
194
+ lifespan=self.registry.lifespan,
194
195
  openapi_tags=[tag.model_dump(by_alias=True) for tag in self.tags.values()]
195
196
  if self.tags
196
197
  else None,
@@ -473,7 +474,7 @@ class GenericConfigurator(Generic[TRegistry]):
473
474
  * `gettext`, `ngettext`, `dgettext`, `dngettext`, `pgettext`, `dpgettext`,
474
475
  `npgettext`, `dnpgettext` methods are installed for i18n purpose.
475
476
  """
476
- lczr = request.registry.localizer(request)
477
+ lczr = request.registry.localizer(request.locale_name)
477
478
  custom_globals = {}
478
479
  for key, (val, evaluate) in self._renderer_globals.items():
479
480
  if evaluate and callable(val):
@@ -124,13 +124,7 @@ def resource(
124
124
  config, method, collection_path, getattr(api, method)
125
125
  )
126
126
  case (
127
- "get"
128
- | "post"
129
- | "put"
130
- | "patch"
131
- | "delete"
132
- | "head"
133
- | "options"
127
+ "get" | "post" | "put" | "patch" | "delete" | "head" | "options"
134
128
  ):
135
129
  bind_config(config, method, path, getattr(api, method))
136
130
  case _:
@@ -72,7 +72,7 @@ class SessionMiddleware(AbstractMiddleware):
72
72
  headers = MutableHeaders(scope=message)
73
73
  expires = "expires=Thu, 01 Jan 1970 00:00:00 GMT; "
74
74
  header_value = (
75
- f"{self.cookie_name}=; " f"{expires}{self.security_flags}"
75
+ f"{self.cookie_name}=; {expires}{self.security_flags}"
76
76
  )
77
77
  headers.append("set-cookie", header_value)
78
78
  await send(message)
@@ -1,5 +1,8 @@
1
- from collections.abc import Mapping
2
- from typing import TYPE_CHECKING, Generic, TypeVar
1
+ from collections.abc import AsyncIterator, Mapping
2
+ from contextlib import asynccontextmanager
3
+ from typing import TYPE_CHECKING, Any, Generic, TypeVar
4
+
5
+ from fastapi import FastAPI
3
6
 
4
7
  if TYPE_CHECKING:
5
8
  from fastlife.service.locale_negociator import LocaleNegociator # coverage: ignore
@@ -22,7 +25,7 @@ class GenericRegistry(Generic[TSettings]):
22
25
  It is initialized by the configurator and accessed by the `fastlife.Registry`.
23
26
  """
24
27
 
25
- settings: Settings
28
+ settings: TSettings
26
29
  """Application settings."""
27
30
  renderers: Mapping[str, "AbstractTemplateRendererFactory"]
28
31
  """Registered template engine."""
@@ -31,7 +34,7 @@ class GenericRegistry(Generic[TSettings]):
31
34
  localizer: "LocalizerFactory"
32
35
  """Used to localized message."""
33
36
 
34
- def __init__(self, settings: Settings) -> None:
37
+ def __init__(self, settings: TSettings) -> None:
35
38
  from fastlife.service.locale_negociator import default_negociator
36
39
  from fastlife.service.translations import LocalizerFactory
37
40
 
@@ -46,6 +49,19 @@ class GenericRegistry(Generic[TSettings]):
46
49
  return val
47
50
  raise RuntimeError(f"No renderer registered for template {template}")
48
51
 
52
+ @asynccontextmanager
53
+ async def lifespan(self, app: FastAPI) -> AsyncIterator[Any]:
54
+ """
55
+ hook to override the lifespan of the starlette app.
56
+
57
+ The [lifespan](https://asgi.readthedocs.io/en/latest/specs/lifespan.html)
58
+ is used to initialized and dispose the application state.
59
+
60
+ In fastlife the application state is the registry, it has to be overriden
61
+ to add an implementation.
62
+ """
63
+ yield
64
+
49
65
 
50
66
  DefaultRegistry = GenericRegistry[Settings]
51
67
  """
@@ -0,0 +1 @@
1
+
@@ -3,18 +3,52 @@ from collections import defaultdict
3
3
  from collections.abc import Callable, Iterator
4
4
  from gettext import GNUTranslations
5
5
  from io import BufferedReader
6
- from typing import TYPE_CHECKING
7
6
 
8
7
  from fastlife.shared_utils.resolver import resolve_path
9
8
 
10
- if TYPE_CHECKING:
11
- from fastlife import Request # coverage: ignore
12
-
13
9
  LocaleName = str
14
10
  Domain = str
15
11
  CONTEXT_ENCODING = "%s\x04%s"
16
12
 
17
13
 
14
+ class TranslatableString(str):
15
+ """
16
+ Create a string made for translation associated to a domain.
17
+ This class is instanciated by the
18
+ :class:`fastlife.service.translations.TranslatableStringFactory` class.
19
+ """
20
+
21
+ __slots__ = ("domain",)
22
+
23
+ def __new__(cls, msgid: str, domain: str) -> "TranslatableString":
24
+ self = str.__new__(cls, msgid)
25
+ self.domain = domain # type: ignore
26
+ return self
27
+
28
+
29
+ class TranslatableStringFactory:
30
+ """Create a catalog of string associated to a domain."""
31
+
32
+ def __init__(self, domain: str):
33
+ self.domain = domain
34
+
35
+ def __call__(self, msgid: str) -> str:
36
+ """
37
+ Use to generate the translatable string.
38
+
39
+ usually:
40
+
41
+ ```python
42
+ _ = TranslatableStringFactory("mydomain")
43
+ mymessage = _("translatable")
44
+ ```
45
+
46
+ Note that the string is associated to mydomain, so the babel extraction has
47
+ to be initialized with that particular domain.
48
+ """
49
+ return TranslatableString(msgid, self.domain)
50
+
51
+
18
52
  def find_mo_files(root_path: str) -> Iterator[tuple[LocaleName, Domain, pathlib.Path]]:
19
53
  """
20
54
  Find .mo files in a locales directory.
@@ -68,7 +102,10 @@ class Localizer:
68
102
  return self.gettext(message, mapping)
69
103
 
70
104
  def gettext(self, message: str, mapping: dict[str, str] | None = None) -> str:
71
- ret = self.global_translations.gettext(message)
105
+ if isinstance(message, TranslatableString):
106
+ ret = self.translations[message.domain].gettext(message) # type: ignore
107
+ else:
108
+ ret = self.global_translations.gettext(message)
72
109
  if mapping:
73
110
  ret = ret.format(**mapping)
74
111
  return ret
@@ -177,8 +214,8 @@ class LocalizerFactory:
177
214
  root_path = resolve_path(path)
178
215
  self._translations.load(root_path)
179
216
 
180
- def __call__(self, request: "Request") -> Localizer:
217
+ def __call__(self, locale_name: LocaleName) -> Localizer:
181
218
  """Create the translation context for the given request."""
182
- if request.locale_name not in self._translations:
219
+ if locale_name not in self._translations:
183
220
  return self.null_localizer
184
- return self._translations.get(request.locale_name)
221
+ return self._translations.get(locale_name)
fastlife/testing/form.py CHANGED
@@ -109,7 +109,7 @@ class WebForm:
109
109
  raise ValueError(f'"{fieldname}" does not exists')
110
110
  field = self._formfields[fieldname]
111
111
  if field.node_name != "select":
112
- raise ValueError(f"{fieldname} is a {field!r}, " "use set() instead")
112
+ raise ValueError(f"{fieldname} is a {field!r}, use set() instead")
113
113
 
114
114
  for option in field.by_node_name("option"):
115
115
  if option.text == value.strip():
@@ -1,7 +1,7 @@
1
1
  """Testing your application."""
2
2
 
3
3
  from collections.abc import Mapping, MutableMapping
4
- from typing import Any, Literal
4
+ from typing import Any, Literal, Self
5
5
  from urllib.parse import urlencode
6
6
 
7
7
  import bs4
@@ -126,7 +126,7 @@ class WebTestClient:
126
126
 
127
127
  @property
128
128
  def session(self) -> MutableMapping[str, Any]:
129
- """Session shared between the server and the client."""
129
+ """Server session stored in a cookies."""
130
130
  return Session(self)
131
131
 
132
132
  def request(
@@ -208,3 +208,10 @@ class WebTestClient:
208
208
  headers={"Content-Type": "application/x-www-form-urlencoded", **headers},
209
209
  max_redirects=int(follow_redirects) * 10,
210
210
  )
211
+
212
+ def __enter__(self) -> Self:
213
+ self.testclient.__enter__()
214
+ return self
215
+
216
+ def __exit__(self, *args: Any, **kwargs: Any) -> None:
217
+ self.testclient.__exit__()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fastlifeweb
3
- Version: 0.23.0
3
+ Version: 0.24.0
4
4
  Summary: High-level web framework
5
5
  Author-Email: Guillaume Gauvrit <guillaume@gauvr.it>
6
6
  License: MIT License
@@ -1,10 +1,10 @@
1
- CHANGELOG.md,sha256=Ap14kfVx07rFhITGDmtQfwEsJowg5osxKETE0w478lU,7801
2
- fastlife/__init__.py,sha256=cx3BScbBelH-Tm63VPdAp5siW1fBZ0uOMwNW_SPs2xQ,2126
1
+ CHANGELOG.md,sha256=1gHbqLrx79ltwysUaPw25Sp6Aqz5nbyZK_qcgJxc6Og,7952
2
+ fastlife/__init__.py,sha256=nXWE4AbhkhG_yBjPJU-XnKDMTsU9ebv7Vj4eIciWQI0,2219
3
3
  fastlife/adapters/__init__.py,sha256=imPD1hImpgrYkvUJRhHA5kVyGAua7VbP2WGkhSWKJT8,93
4
4
  fastlife/adapters/fastapi/__init__.py,sha256=1goV1FGFP04TGyskJBLKZam4Gvt1yoAvLMNs4ekWSSQ,243
5
5
  fastlife/adapters/fastapi/form.py,sha256=csxsDI6RK-g41pMwFhaVQCLDhF7dAZzgUp-VcrC3NFY,823
6
6
  fastlife/adapters/fastapi/form_data.py,sha256=2DQ0o-RvY6iROUKQjS-UJdNYEVSsNPd-AjpergI3w54,4473
7
- fastlife/adapters/fastapi/localizer.py,sha256=XD1kCJuAlkGevivmvAJEcGMCBWMef9rAfTOGmt3PVWU,436
7
+ fastlife/adapters/fastapi/localizer.py,sha256=Efn6rrf-SnSfM4TqqE_5chacrxaPpupxbvIqXipXEEw,448
8
8
  fastlife/adapters/fastapi/request.py,sha256=COOoSMZAm4VhyJgM7dlqJ7YdGjeGI7qs93PtBsriEPc,1115
9
9
  fastlife/adapters/fastapi/routing/__init__.py,sha256=8EMnQE5n8oA4J9_c3nxzwKDVt3tefZ6fGH0d2owE8mo,195
10
10
  fastlife/adapters/fastapi/routing/route.py,sha256=XnDPvd5V0Zl7Ke6bBErEtUCjmNQPcV2U_w1dWpx6qM4,1476
@@ -1686,10 +1686,10 @@ fastlife/components/pydantic_form/FatalError.jinja,sha256=lFVlNrXzBR6ExMahq77h0t
1686
1686
  fastlife/components/pydantic_form/Hint.jinja,sha256=8leBpfMGDmalc_KAjr2paTojr_rwq-luS6m_1BGj7Tw,202
1687
1687
  fastlife/components/pydantic_form/Widget.jinja,sha256=PgguUpvhG6CY9AW6H8qQMjKqjlybjDCAaFFAOHzrzVQ,418
1688
1688
  fastlife/config/__init__.py,sha256=5qpuaVYqi-AS0GgsfggM6rFsSwXgrqrLBo9jH6dVroc,407
1689
- fastlife/config/configurator.py,sha256=SURXmBrdTghHoG2f9R2BUF6TKZXg1lNmwP3ZbuApJ3M,24722
1689
+ fastlife/config/configurator.py,sha256=wlgSfbKoeBzk5kmCnftmrgVV8CWQhxoioBZeuRTPZ64,24779
1690
1690
  fastlife/config/exceptions.py,sha256=9MdBnbfy-Aw-KaIFzju0Kh8Snk41-v9LqK2w48Tdy1s,1169
1691
1691
  fastlife/config/openapiextra.py,sha256=rYoerrn9sni2XwnO3gIWqaz7M0aDZPhVLjzqhDxue0o,514
1692
- fastlife/config/resources.py,sha256=u6OgnbHfGkC5idH-YPNkIPf8GJnZpJoGVZ-Ym022BCo,8533
1692
+ fastlife/config/resources.py,sha256=EcPTM25pnHcGFTtXjeZnWn5Mo_-8rhJ72HJ6rxnjPg8,8389
1693
1693
  fastlife/config/views.py,sha256=9CZ0qNi8vKvQuGo1GgM6cwNK8WwHOxwIHqtikAOaOHY,2399
1694
1694
  fastlife/domain/__init__.py,sha256=3zDDos5InVX0el9OO0lgSDGzdUNYIhlA6w4uhBh2pF8,29
1695
1695
  fastlife/domain/model/__init__.py,sha256=aoBjaSpDscuFXvtknJHwiNyoJRUpE-v4X54h_wNuo2Y,27
@@ -1705,17 +1705,18 @@ fastlife/middlewares/base.py,sha256=7FZE_1YU7wNew2u1qdYXjamosk4CXJmg1mJWGp6Xhc0,
1705
1705
  fastlife/middlewares/reverse_proxy/__init__.py,sha256=g1SoVDmenKzpAAPYHTEsWgdBByOxtLg9fGx6RV3i0ok,846
1706
1706
  fastlife/middlewares/reverse_proxy/x_forwarded.py,sha256=PPDjcfwik5eoYaolSY1Y4x5QMKpDV0XrOP_i4Am0y30,1724
1707
1707
  fastlife/middlewares/session/__init__.py,sha256=ZhXWXs53A__F9wJKBJ87rW8Qyt5Mn866vhzKDxVZ4t0,1348
1708
- fastlife/middlewares/session/middleware.py,sha256=ituZ5hNipDMkgCXNE4zbnmOcWEF151sucPo4I_FpY-I,3137
1708
+ fastlife/middlewares/session/middleware.py,sha256=MyZ0MobhlnGiqacYq0pPYBrlqCjTkWjpNXNeFyPD1fI,3133
1709
1709
  fastlife/middlewares/session/serializer.py,sha256=nbJGiCJ_ryZxkW1I28kmK6hD3U98D4ZlUQA7B8_tngQ,635
1710
1710
  fastlife/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1711
1711
  fastlife/service/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1712
1712
  fastlife/service/check_permission.py,sha256=-TsI58YZJtWIw5bsm0fVpfuaCMUx4cmoLTKGXeyQPDk,1809
1713
1713
  fastlife/service/csrf.py,sha256=wC1PaKOmZ3il0FF_kevxnlg9PxDqruRdLrNnOA3ZHrU,1886
1714
1714
  fastlife/service/locale_negociator.py,sha256=JUqzTukxDqTJVOR-CNI7Vqo6kvdvwxYvZQe8P3V9S2U,796
1715
- fastlife/service/registry.py,sha256=B6n5b_b0RgxJj0qFOpnrJFmG7_MPtvShwV6yH9V6vi0,2098
1715
+ fastlife/service/registry.py,sha256=3lm7aUD7FdlV1lQUS73OuBlL55-O1DulPJdE0n6Epks,2648
1716
1716
  fastlife/service/security_policy.py,sha256=qYXs4mhfz_u4x59NhUkirqKYKQbFv9YrzyRuXj7mxE0,4688
1717
1717
  fastlife/service/templates.py,sha256=QPAIUbbZiekazz_jV3q4JCwQd6Q4KA6a4RDek2RWuhE,2548
1718
- fastlife/service/translations.py,sha256=D-1D3pVNytEcps1u-0K7FmgQ8Wo6Yu4XVHvZrPhBmAI,5795
1718
+ fastlife/service/translatablestring.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
1719
+ fastlife/service/translations.py,sha256=s6qFZSXR-1vYxSr7RRH-mS-VjNaa8OTxR7-k6Ib7h0E,6878
1719
1720
  fastlife/settings.py,sha256=q-rz4CEF2RQGow5-m-yZJOvdh3PPb2c1Q_ZLJGnu4VQ,3647
1720
1721
  fastlife/shared_utils/__init__.py,sha256=i66ytuf-Ezo7jSiNQHIsBMVIcB-tDX0tg28-pUOlhzE,26
1721
1722
  fastlife/shared_utils/infer.py,sha256=0GflLkaWJ-4LZ1Ig3moR-_o55wwJ_p_vJ4xo-yi3lyA,1406
@@ -1723,14 +1724,14 @@ fastlife/shared_utils/resolver.py,sha256=Wb9cO2MWavpti63hju15xmwFMgaD5DsQaxikRpB
1723
1724
  fastlife/template_globals.py,sha256=rJ7XB9DlPubejs_GDlEyO9V98QZTJnmdTznlguYOyoc,8386
1724
1725
  fastlife/testing/__init__.py,sha256=VpxkS3Zp3t_hH8dBiLaGFGhsvt511dhBS_8fMoFXdmU,99
1725
1726
  fastlife/testing/dom.py,sha256=dVzDoZokn-ii681UaEwAr-khM5KE-CHgXSSLSo24oH0,4489
1726
- fastlife/testing/form.py,sha256=ST0xNCoUqz_oD92cWHzQ6CbJ5hFopvu_NNKpOfiuYWY,7874
1727
+ fastlife/testing/form.py,sha256=diiGfVMfNt19JTNUxlnbGfcbskR3ZMpk0Y-A57vfShc,7871
1727
1728
  fastlife/testing/session.py,sha256=LEFFbiR67_x_g-ioudkY0C7PycHdbDfaIaoo_G7GXQ8,2226
1728
- fastlife/testing/testclient.py,sha256=JTIgeMKooA8L4gEodeC3gy4Lo27y3WNswSEIKLlVVPs,6745
1729
+ fastlife/testing/testclient.py,sha256=Id1tlA1ZapyW-8kUh2_U3lLteL64m3ERqOO7NAN7HEY,6922
1729
1730
  fastlife/views/__init__.py,sha256=zG8gveL8e2zBdYx6_9jtZfpQ6qJT-MFnBY3xXkLwHZI,22
1730
1731
  fastlife/views/pydantic_form.py,sha256=o7EUItciAGL1OSaGNHo-3BTrYAk34GuWE7zGikjiAGY,1486
1731
- fastlifeweb-0.23.0.dist-info/METADATA,sha256=Eww8hBxH7oR5_EeqtALpdmUmJr2f1vs1E0EijXaxM30,3663
1732
- fastlifeweb-0.23.0.dist-info/WHEEL,sha256=thaaA2w1JzcGC48WYufAs8nrYZjJm8LqNfnXFOFyCC4,90
1733
- fastlifeweb-0.23.0.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
1734
- fastlifeweb-0.23.0.dist-info/licenses/LICENSE,sha256=NlRX9Z-dcv8X1VFW9odlIQBbgNN9pcO94XzvKp2R16o,1075
1732
+ fastlifeweb-0.24.0.dist-info/METADATA,sha256=a4Pp2mA7yCIvBnvF7Jrrt0jzQRX7ousymKDpb1slGfY,3663
1733
+ fastlifeweb-0.24.0.dist-info/WHEEL,sha256=thaaA2w1JzcGC48WYufAs8nrYZjJm8LqNfnXFOFyCC4,90
1734
+ fastlifeweb-0.24.0.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
1735
+ fastlifeweb-0.24.0.dist-info/licenses/LICENSE,sha256=NlRX9Z-dcv8X1VFW9odlIQBbgNN9pcO94XzvKp2R16o,1075
1735
1736
  tailwind.config.js,sha256=EN3EahBDmQBbmJvkw3SdGWNOkfkzw0cg-QvBikOhkrw,1348
1736
- fastlifeweb-0.23.0.dist-info/RECORD,,
1737
+ fastlifeweb-0.24.0.dist-info/RECORD,,