fastlifeweb 0.16.3__py3-none-any.whl → 0.17.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 (56) hide show
  1. fastlife/adapters/jinjax/renderer.py +49 -25
  2. fastlife/adapters/jinjax/widget_factory/__init__.py +1 -0
  3. fastlife/adapters/jinjax/widget_factory/base.py +38 -0
  4. fastlife/adapters/jinjax/widget_factory/bool_builder.py +43 -0
  5. fastlife/adapters/jinjax/widget_factory/emailstr_builder.py +46 -0
  6. fastlife/adapters/jinjax/widget_factory/enum_builder.py +47 -0
  7. fastlife/adapters/jinjax/widget_factory/factory.py +165 -0
  8. fastlife/adapters/jinjax/widget_factory/literal_builder.py +52 -0
  9. fastlife/adapters/jinjax/widget_factory/model_builder.py +64 -0
  10. fastlife/adapters/jinjax/widget_factory/secretstr_builder.py +47 -0
  11. fastlife/adapters/jinjax/widget_factory/sequence_builder.py +58 -0
  12. fastlife/adapters/jinjax/widget_factory/set_builder.py +80 -0
  13. fastlife/adapters/jinjax/widget_factory/simpletype_builder.py +47 -0
  14. fastlife/adapters/jinjax/widget_factory/union_builder.py +90 -0
  15. fastlife/adapters/jinjax/widgets/base.py +6 -4
  16. fastlife/adapters/jinjax/widgets/checklist.py +1 -1
  17. fastlife/adapters/jinjax/widgets/dropdown.py +7 -7
  18. fastlife/adapters/jinjax/widgets/hidden.py +2 -0
  19. fastlife/adapters/jinjax/widgets/model.py +4 -1
  20. fastlife/adapters/jinjax/widgets/sequence.py +3 -2
  21. fastlife/adapters/jinjax/widgets/text.py +9 -10
  22. fastlife/adapters/jinjax/widgets/union.py +9 -7
  23. fastlife/components/Form.jinja +12 -0
  24. fastlife/config/configurator.py +23 -24
  25. fastlife/config/exceptions.py +4 -1
  26. fastlife/config/openapiextra.py +1 -0
  27. fastlife/config/resources.py +26 -27
  28. fastlife/config/settings.py +2 -0
  29. fastlife/config/views.py +3 -1
  30. fastlife/middlewares/reverse_proxy/x_forwarded.py +22 -15
  31. fastlife/middlewares/session/middleware.py +2 -2
  32. fastlife/middlewares/session/serializer.py +6 -5
  33. fastlife/request/form.py +7 -6
  34. fastlife/request/form_data.py +2 -6
  35. fastlife/routing/route.py +3 -1
  36. fastlife/routing/router.py +1 -0
  37. fastlife/security/csrf.py +2 -1
  38. fastlife/security/policy.py +2 -1
  39. fastlife/services/locale_negociator.py +2 -1
  40. fastlife/services/policy.py +3 -2
  41. fastlife/services/templates.py +2 -1
  42. fastlife/services/translations.py +15 -8
  43. fastlife/shared_utils/infer.py +4 -3
  44. fastlife/shared_utils/resolver.py +64 -4
  45. fastlife/templates/binding.py +2 -1
  46. fastlife/testing/__init__.py +1 -0
  47. fastlife/testing/dom.py +140 -0
  48. fastlife/testing/form.py +204 -0
  49. fastlife/testing/session.py +67 -0
  50. fastlife/testing/testclient.py +7 -390
  51. fastlife/views/pydantic_form.py +4 -4
  52. {fastlifeweb-0.16.3.dist-info → fastlifeweb-0.17.0.dist-info}/METADATA +6 -6
  53. {fastlifeweb-0.16.3.dist-info → fastlifeweb-0.17.0.dist-info}/RECORD +55 -40
  54. fastlife/adapters/jinjax/widgets/factory.py +0 -525
  55. {fastlifeweb-0.16.3.dist-info → fastlifeweb-0.17.0.dist-info}/LICENSE +0 -0
  56. {fastlifeweb-0.16.3.dist-info → fastlifeweb-0.17.0.dist-info}/WHEEL +0 -0
@@ -8,7 +8,8 @@ API Resources declaration using a decorator.
8
8
 
9
9
  """
10
10
 
11
- from typing import Any, Callable
11
+ from collections.abc import Callable
12
+ from typing import Any
12
13
 
13
14
  import venusian
14
15
  from fastapi.types import IncEx
@@ -54,7 +55,7 @@ def resource(
54
55
 
55
56
  Note that there is no abstract class that declare this method, this is done by
56
57
  introspection while returning the configuration method
57
- {meth}`fastlife.config.configurator.Configurator.include`
58
+ {meth}`fastlife.config.configurator.GenericConfigurator.include`
58
59
  """
59
60
  tag = name
60
61
 
@@ -67,7 +68,7 @@ def resource(
67
68
 
68
69
  config: Configurator = getattr(scanner, VENUSIAN_CATEGORY)
69
70
  if description:
70
- config.add_open_tag(
71
+ config.add_openapi_tag(
71
72
  OpenApiTag(
72
73
  name=tag, description=description, externalDocs=external_docs
73
74
  )
@@ -91,29 +92,21 @@ def resource(
91
92
  endpoint=endpoint,
92
93
  tags=[tag],
93
94
  methods=[method.split("_").pop()],
94
- permission=getattr(endpoint, "permission"),
95
- status_code=getattr(endpoint, "status_code"),
96
- summary=getattr(endpoint, "summary"),
97
- description=getattr(endpoint, "description"),
98
- response_description=getattr(endpoint, "response_description"),
99
- deprecated=getattr(endpoint, "deprecated"),
100
- operation_id=getattr(endpoint, "operation_id"),
101
- response_model_include=getattr(endpoint, "response_model_include"),
102
- response_model_exclude=getattr(endpoint, "response_model_exclude"),
103
- response_model_by_alias=getattr(
104
- endpoint, "response_model_by_alias"
105
- ),
106
- response_model_exclude_unset=getattr(
107
- endpoint, "response_model_exclude_unset"
108
- ),
109
- response_model_exclude_defaults=getattr(
110
- endpoint, "response_model_exclude_defaults"
111
- ),
112
- response_model_exclude_none=getattr(
113
- endpoint, "response_model_exclude_none"
114
- ),
115
- include_in_schema=getattr(endpoint, "include_in_schema"),
116
- openapi_extra=getattr(endpoint, "openapi_extra"),
95
+ permission=endpoint.permission,
96
+ status_code=endpoint.status_code,
97
+ summary=endpoint.summary,
98
+ description=endpoint.description,
99
+ response_description=endpoint.response_description,
100
+ deprecated=endpoint.deprecated,
101
+ operation_id=endpoint.operation_id,
102
+ response_model_include=endpoint.response_model_include,
103
+ response_model_exclude=endpoint.response_model_exclude,
104
+ response_model_by_alias=endpoint.response_model_by_alias,
105
+ response_model_exclude_unset=endpoint.response_model_exclude_unset,
106
+ response_model_exclude_defaults=endpoint.response_model_exclude_defaults,
107
+ response_model_exclude_none=endpoint.response_model_exclude_none,
108
+ include_in_schema=endpoint.include_in_schema,
109
+ openapi_extra=endpoint.openapi_extra,
117
110
  )
118
111
 
119
112
  for method in dir(ob):
@@ -131,7 +124,13 @@ def resource(
131
124
  config, method, collection_path, getattr(api, method)
132
125
  )
133
126
  case (
134
- "get" | "post" | "put" | "patch" | "delete" | "head" | "options"
127
+ "get"
128
+ | "post"
129
+ | "put"
130
+ | "patch"
131
+ | "delete"
132
+ | "head"
133
+ | "options"
135
134
  ):
136
135
  bind_config(config, method, path, getattr(api, method))
137
136
  case _:
@@ -74,6 +74,8 @@ class Settings(BaseSettings):
74
74
  )
75
75
  """
76
76
  Set global constants accessible in every templates.
77
+ Defaults to `fastlife.templates.constants:Constants`
78
+ See {class}`fastlife.templates.constants.Constants`
77
79
  """
78
80
 
79
81
  session_secret_key: str = Field(default="")
fastlife/config/views.py CHANGED
@@ -17,7 +17,9 @@ async def hello_world(
17
17
  return template()
18
18
  ```
19
19
  """
20
- from typing import Any, Callable
20
+
21
+ from collections.abc import Callable
22
+ from typing import Any
21
23
 
22
24
  import venusian
23
25
 
@@ -1,5 +1,5 @@
1
1
  import logging
2
- from typing import Optional, Sequence, Tuple
2
+ from collections.abc import Sequence
3
3
 
4
4
  from starlette.types import ASGIApp, Receive, Scope, Send
5
5
 
@@ -8,21 +8,20 @@ from fastlife.middlewares.base import AbstractMiddleware
8
8
  log = logging.getLogger(__name__)
9
9
 
10
10
 
11
- def get_header(headers: Sequence[Tuple[bytes, bytes]], key: bytes) -> Optional[str]:
12
- for hdr in headers:
13
- if hdr[0].lower() == key:
14
- return hdr[1].decode("latin1")
11
+ def get_header(headers: Sequence[tuple[bytes, bytes]], key: bytes) -> str | None:
12
+ for hkey, val in headers:
13
+ if hkey.lower() == key:
14
+ return val.decode("latin1")
15
15
  return None
16
16
 
17
17
 
18
- def get_header_int(headers: Sequence[Tuple[bytes, bytes]], key: bytes) -> Optional[int]:
19
- for hdr in headers:
20
- if hdr[0].lower() == key:
21
- ret = hdr[1].decode("latin1")
18
+ def get_header_int(headers: Sequence[tuple[bytes, bytes]], key: bytes) -> int | None:
19
+ for hkey, val in headers:
20
+ if hkey.lower() == key:
22
21
  try:
23
- return int(ret)
22
+ return int(val.decode("latin1"))
24
23
  except ValueError:
25
- pass
24
+ break
26
25
  return None
27
26
 
28
27
 
@@ -36,11 +35,19 @@ class XForwardedStar(AbstractMiddleware):
36
35
  async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
37
36
  if scope["type"] in ("http", "websocket"):
38
37
  headers = scope["headers"]
38
+ x_real_ip = get_header(headers, b"x-real-ip")
39
+ client = (
40
+ (
41
+ x_real_ip,
42
+ get_header_int(headers, b"x-real-port")
43
+ or get_header_int(headers, b"x-forwarded-port")
44
+ or 0,
45
+ )
46
+ if x_real_ip
47
+ else None
48
+ )
39
49
  new_vals = {
40
- "client": (
41
- get_header(headers, b"x-real-ip"),
42
- get_header_int(headers, b"x-forwarded-port"),
43
- ),
50
+ "client": client,
44
51
  "host": get_header(headers, b"x-forwarded-host"),
45
52
  "scheme": get_header(headers, b"x-forwarded-proto"),
46
53
  }
@@ -1,7 +1,7 @@
1
1
  """Deal with http session."""
2
2
 
3
3
  from datetime import timedelta
4
- from typing import Literal, Type
4
+ from typing import Literal
5
5
 
6
6
  from starlette.datastructures import MutableHeaders
7
7
  from starlette.requests import HTTPConnection
@@ -25,7 +25,7 @@ class SessionMiddleware(AbstractMiddleware):
25
25
  cookie_same_site: Literal["lax", "strict", "none"] = "lax",
26
26
  cookie_secure: bool = False,
27
27
  cookie_domain: str = "",
28
- serializer: Type[AbsractSessionSerializer] = SignedSessionSerializer,
28
+ serializer: type[AbsractSessionSerializer] = SignedSessionSerializer,
29
29
  ) -> None:
30
30
  self.app = app
31
31
  self.max_age = int(duration.total_seconds())
@@ -1,8 +1,10 @@
1
1
  """Serialize session."""
2
+
2
3
  import abc
3
4
  import json
4
5
  from base64 import b64decode, b64encode
5
- from typing import Any, Mapping, Tuple
6
+ from collections.abc import Mapping
7
+ from typing import Any
6
8
 
7
9
  import itsdangerous
8
10
 
@@ -11,8 +13,7 @@ class AbsractSessionSerializer(abc.ABC):
11
13
  """Session serializer base class"""
12
14
 
13
15
  @abc.abstractmethod
14
- def __init__(self, secret_key: str, max_age: int) -> None:
15
- ...
16
+ def __init__(self, secret_key: str, max_age: int) -> None: ...
16
17
 
17
18
  @abc.abstractmethod
18
19
  def serialize(self, data: Mapping[str, Any]) -> bytes:
@@ -20,7 +21,7 @@ class AbsractSessionSerializer(abc.ABC):
20
21
  ...
21
22
 
22
23
  @abc.abstractmethod
23
- def deserialize(self, data: bytes) -> Tuple[Mapping[str, Any], bool]:
24
+ def deserialize(self, data: bytes) -> tuple[Mapping[str, Any], bool]:
24
25
  """Derialize the session raw bytes content and return it as a mapping."""
25
26
  ...
26
27
 
@@ -47,7 +48,7 @@ class SignedSessionSerializer(AbsractSessionSerializer):
47
48
  signed = self.signer.sign(encoded)
48
49
  return signed
49
50
 
50
- def deserialize(self, data: bytes) -> Tuple[Mapping[str, Any], bool]:
51
+ def deserialize(self, data: bytes) -> tuple[Mapping[str, Any], bool]:
51
52
  """Deserialize the session.
52
53
 
53
54
  If the signature is incorect, the session restart from the begining.
fastlife/request/form.py CHANGED
@@ -1,6 +1,7 @@
1
1
  """HTTP Form serialization."""
2
2
 
3
- from typing import Any, Callable, Generic, Mapping, Type, TypeVar, get_origin
3
+ from collections.abc import Callable, Mapping
4
+ from typing import Any, Generic, TypeVar, get_origin
4
5
 
5
6
  from fastapi import Depends
6
7
  from pydantic import BaseModel, ValidationError
@@ -28,7 +29,7 @@ class FormModel(Generic[T]):
28
29
  self.is_valid = is_valid
29
30
 
30
31
  @classmethod
31
- def default(cls, prefix: str, pydantic_type: Type[T]) -> "FormModel[T]":
32
+ def default(cls, prefix: str, pydantic_type: type[T]) -> "FormModel[T]":
32
33
  return cls(prefix, pydantic_type.model_construct(), {})
33
34
 
34
35
  def edit(self, pydantic_type: T) -> None:
@@ -47,7 +48,7 @@ class FormModel(Generic[T]):
47
48
 
48
49
  @classmethod
49
50
  def from_payload(
50
- cls, prefix: str, pydantic_type: Type[T], data: Mapping[str, Any]
51
+ cls, prefix: str, pydantic_type: type[T], data: Mapping[str, Any]
51
52
  ) -> "FormModel[T]":
52
53
  try:
53
54
  return cls(prefix, pydantic_type(**data.get(prefix, {})), {}, True)
@@ -68,12 +69,12 @@ class FormModel(Generic[T]):
68
69
  continue
69
70
 
70
71
  else:
71
- raise NotImplementedError
72
+ raise NotImplementedError from exc # coverage: ignore
72
73
  elif issubclass(typ, BaseModel):
73
74
  typ = typ.model_fields[part].annotation
74
75
  loc = f"{loc}.{part}"
75
76
  else:
76
- raise NotImplementedError
77
+ raise NotImplementedError from exc # coverage: ignore
77
78
 
78
79
  else:
79
80
  # it is an integer and it part of the list
@@ -88,7 +89,7 @@ class FormModel(Generic[T]):
88
89
 
89
90
 
90
91
  def form_model(
91
- cls: Type[T], name: str | None = None
92
+ cls: type[T], name: str | None = None
92
93
  ) -> Callable[[Mapping[str, Any]], FormModel[T]]:
93
94
  """
94
95
  Build a model, a class of type T based on Pydandic Base Model from a form payload.
@@ -2,14 +2,10 @@
2
2
  Set of functions to unserialize www-form-urlencoded format to python simple types.
3
3
  """
4
4
 
5
+ from collections.abc import Mapping, MutableMapping, MutableSequence, Sequence
5
6
  from typing import (
6
7
  Annotated,
7
8
  Any,
8
- Mapping,
9
- MutableMapping,
10
- MutableSequence,
11
- Optional,
12
- Sequence,
13
9
  )
14
10
 
15
11
  from fastapi import Depends
@@ -22,7 +18,7 @@ def unflatten_struct(
22
18
  unflattened_output: MutableMapping[str, Any] | MutableSequence[Any],
23
19
  level: int = 0,
24
20
  *,
25
- csrf_token_name: Optional[str] = None,
21
+ csrf_token_name: str | None = None,
26
22
  ) -> Mapping[str, Any] | Sequence[Any]:
27
23
  """
28
24
  Take a flatten_input map, with key segmented by `.` and build a nested dict.
fastlife/routing/route.py CHANGED
@@ -1,5 +1,7 @@
1
1
  """HTTP Route."""
2
- from typing import TYPE_CHECKING, Any, Callable, Coroutine
2
+
3
+ from collections.abc import Callable, Coroutine
4
+ from typing import TYPE_CHECKING, Any
3
5
 
4
6
  from fastapi.routing import APIRoute
5
7
  from starlette.requests import Request as StarletteRequest
@@ -4,6 +4,7 @@ FastApi router for fastlife application.
4
4
  The aim of this router is get {class}`fastlife.routing.route.Route`
5
5
  available in the FastApi request depency injection.
6
6
  """
7
+
7
8
  from typing import Any
8
9
 
9
10
  from fastapi import APIRouter
fastlife/security/csrf.py CHANGED
@@ -19,7 +19,8 @@ no way to prevent to set the cookie in the request.
19
19
  """
20
20
 
21
21
  import secrets
22
- from typing import Any, Callable, Coroutine
22
+ from collections.abc import Callable, Coroutine
23
+ from typing import Any
23
24
 
24
25
  from fastlife.request import Request
25
26
 
@@ -2,7 +2,8 @@
2
2
 
3
3
  import abc
4
4
  import logging
5
- from typing import Annotated, Any, Callable, Coroutine, Generic, Literal, TypeVar
5
+ from collections.abc import Callable, Coroutine
6
+ from typing import Annotated, Any, Generic, Literal, TypeVar
6
7
  from uuid import UUID
7
8
 
8
9
  from fastapi import Depends, HTTPException
@@ -1,6 +1,7 @@
1
1
  """Find the localization gor the given request."""
2
2
 
3
- from typing import TYPE_CHECKING, Any, Callable
3
+ from collections.abc import Callable
4
+ from typing import TYPE_CHECKING, Any
4
5
 
5
6
  from fastlife.config.settings import Settings
6
7
 
@@ -1,6 +1,7 @@
1
1
  """Security policy."""
2
2
 
3
- from typing import Any, Callable, Coroutine
3
+ from collections.abc import Callable, Coroutine
4
+ from typing import Any
4
5
 
5
6
  CheckPermissionHook = Callable[..., Coroutine[Any, Any, None]] | Callable[..., None]
6
7
  CheckPermission = Callable[[str], CheckPermissionHook]
@@ -12,7 +13,7 @@ def check_permission(permission_name: str) -> CheckPermissionHook:
12
13
 
13
14
  Adding a permission on the route requires that a security policy has been
14
15
  added using the method
15
- {meth}`fastlife.config.configurator.Configurator.set_security_policy`
16
+ {meth}`fastlife.config.configurator.GenericConfigurator.set_security_policy`
16
17
 
17
18
  :param permission_name: a permission name set in a view to check access.
18
19
  :return: a function that raise http exceptions or any configured exception here.
@@ -10,7 +10,8 @@ In that case, those base classes have to be implemented.
10
10
  """
11
11
 
12
12
  import abc
13
- from typing import Any, Callable, Mapping
13
+ from collections.abc import Callable, Mapping
14
+ from typing import Any
14
15
 
15
16
  from fastlife import Request, Response
16
17
  from fastlife.security.csrf import create_csrf_token
@@ -1,5 +1,6 @@
1
1
  import pathlib
2
- from typing import TYPE_CHECKING, Iterator, Tuple
2
+ from collections.abc import Iterator
3
+ from typing import TYPE_CHECKING
3
4
 
4
5
  from babel.support import NullTranslations, Translations
5
6
 
@@ -11,16 +12,22 @@ if TYPE_CHECKING:
11
12
  locale_name = str
12
13
 
13
14
 
14
- def find_mo_files(root_path: str) -> Iterator[Tuple[str, str, pathlib.Path]]:
15
+ def find_mo_files(root_path: str) -> Iterator[tuple[str, str, pathlib.Path]]:
16
+ """
17
+ Find .mo files in a locales directory.
18
+
19
+ :param root_path: locales directory.
20
+ :return: a tupple containing locale_name, domain, file.
21
+ """
15
22
  root = pathlib.Path(root_path)
16
23
 
17
- # Walk through the directory structure and match the pattern
18
24
  for locale_dir in root.iterdir():
19
- if locale_dir.is_dir(): # Ensure it's a directory (locale)
20
- lc_messages_dir = locale_dir / "LC_MESSAGES"
21
- if lc_messages_dir.exists() and lc_messages_dir.is_dir():
22
- for mo_file in lc_messages_dir.glob("*.mo"): # Find .mo files
23
- yield locale_dir.name, mo_file.name[:-3], mo_file
25
+ lc_messages_dir = locale_dir / "LC_MESSAGES"
26
+ if not (locale_dir.is_dir() and lc_messages_dir.is_dir()):
27
+ continue
28
+
29
+ for mo_file in lc_messages_dir.glob("*.mo"):
30
+ yield locale_dir.name, mo_file.stem, mo_file
24
31
 
25
32
 
26
33
  class Localizer:
@@ -1,11 +1,12 @@
1
1
  """Type inference."""
2
+
2
3
  from types import UnionType
3
- from typing import Any, Type, Union, get_origin
4
+ from typing import Any, Union, get_origin
4
5
 
5
6
  from pydantic import BaseModel
6
7
 
7
8
 
8
- def is_complex_type(typ: Type[Any]) -> bool:
9
+ def is_complex_type(typ: type[Any]) -> bool:
9
10
  """
10
11
  Used to detect complex type such as Mapping, Sequence and pydantic BaseModel.
11
12
 
@@ -14,7 +15,7 @@ def is_complex_type(typ: Type[Any]) -> bool:
14
15
  return bool(get_origin(typ) or issubclass(typ, BaseModel))
15
16
 
16
17
 
17
- def is_union(typ: Type[Any]) -> bool:
18
+ def is_union(typ: type[Any]) -> bool:
18
19
  """Used to detect unions like Optional[T], Union[T, U] or T | U."""
19
20
  type_origin = get_origin(typ)
20
21
  if type_origin:
@@ -1,7 +1,9 @@
1
1
  """Resolution of python objects for dependency injection and more."""
2
+
2
3
  import importlib.util
4
+ import inspect
3
5
  from pathlib import Path
4
- from types import UnionType
6
+ from types import ModuleType, UnionType
5
7
  from typing import Any, Union
6
8
 
7
9
 
@@ -20,8 +22,10 @@ def resolve(value: str) -> Any:
20
22
 
21
23
  try:
22
24
  attr = getattr(module, attr_name)
23
- except AttributeError:
24
- raise ValueError(f"Attribute {attr_name} not found in module {module_name}")
25
+ except AttributeError as exc:
26
+ raise ValueError(
27
+ f"Attribute {attr_name} not found in module {module_name}"
28
+ ) from exc
25
29
 
26
30
  return attr
27
31
 
@@ -32,7 +36,7 @@ def resolve_extended(value: str) -> UnionType:
32
36
  if len(values) == 1:
33
37
  return resolve(value)
34
38
  types = [resolve(t) for t in values if t != "builtins:NoneType"]
35
- return Union[tuple(types)] # type: ignore
39
+ return Union[tuple(types)] # type: ignore # noqa: UP007
36
40
 
37
41
 
38
42
  def resolve_path(value: str) -> str:
@@ -48,3 +52,59 @@ def resolve_path(value: str) -> str:
48
52
  package_path = spec.origin
49
53
  full_path = Path(package_path).parent / resource_name
50
54
  return str(full_path)
55
+
56
+
57
+ def resolve_package(mod: ModuleType) -> ModuleType:
58
+ """
59
+ Return the
60
+ [regular package](https://docs.python.org/3/glossary.html#term-regular-package)
61
+ of a module or itself if it is the ini file of a package.
62
+
63
+ """
64
+
65
+ # Compiled package has no __file__ attribute, ModuleType declare it as NoneType
66
+ if not hasattr(mod, "__file__") or mod.__file__ is None:
67
+ return mod
68
+
69
+ module_path = Path(mod.__file__)
70
+ if module_path.name == "__init__.py":
71
+ return mod
72
+
73
+ parent_module_name = mod.__name__.rsplit(".", 1)[0]
74
+ parent_module = importlib.import_module(parent_module_name)
75
+ return parent_module
76
+
77
+
78
+ def _strip_left_dots(s: str) -> tuple[str, int]:
79
+ stripped_string = s.lstrip(".")
80
+ num_stripped_dots = len(s) - len(stripped_string)
81
+ return stripped_string, num_stripped_dots - 1
82
+
83
+
84
+ def _get_parent(pkg: str, num_parents: int) -> str:
85
+ if num_parents == 0:
86
+ return pkg
87
+ segments = pkg.split(".")
88
+ return ".".join(segments[:-num_parents])
89
+
90
+
91
+ def resolve_maybe_relative(mod: str, stack_depth: int = 1) -> ModuleType:
92
+ """
93
+ Resolve a module, maybe relative to the stack frame and import it.
94
+
95
+ :param mod: the module to import. starts with a dot if it is relative.
96
+ :param stack_depth: relative to which module in the stack.
97
+ used to do an api that call it instead of resolve the module directly.
98
+ :return: the imported module
99
+ """
100
+ if mod.startswith("."):
101
+ caller_module = inspect.getmodule(inspect.stack()[stack_depth][0])
102
+ # we could do an assert here but caller_module could really be none ?
103
+ parent_module = resolve_package(caller_module) # type: ignore
104
+ package = parent_module.__name__
105
+ mod, count = _strip_left_dots(mod)
106
+ package = _get_parent(package, count)
107
+ mod = f"{package}.{mod}".rstrip(".")
108
+
109
+ module = importlib.import_module(mod)
110
+ return module
@@ -2,7 +2,8 @@
2
2
  Bind template to the view in order to build an html response.
3
3
  """
4
4
 
5
- from typing import Any, Callable
5
+ from collections.abc import Callable
6
+ from typing import Any
6
7
 
7
8
  from fastapi import Depends, Response
8
9
 
@@ -1,4 +1,5 @@
1
1
  """Testing fastlife client."""
2
+
2
3
  from .testclient import WebTestClient
3
4
 
4
5
  __all__ = ["WebTestClient"]