fastlifeweb 0.2.3__py3-none-any.whl → 0.3.1__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 (63) hide show
  1. fastlife/__init__.py +5 -0
  2. fastlife/configurator/configurator.py +2 -2
  3. fastlife/configurator/registry.py +7 -3
  4. fastlife/configurator/settings.py +1 -1
  5. fastlife/request/form_data.py +2 -7
  6. fastlife/security/csrf.py +1 -1
  7. fastlife/session/middleware.py +0 -1
  8. fastlife/shared_utils/__init__.py +0 -0
  9. fastlife/shared_utils/resolver.py +5 -3
  10. fastlife/templates/A.jinja +5 -0
  11. fastlife/templates/Button.jinja +27 -0
  12. fastlife/templates/Checkbox.jinja +2 -0
  13. fastlife/templates/CsrfToken.jinja +2 -0
  14. fastlife/templates/Form.jinja +5 -0
  15. fastlife/templates/H1.jinja +4 -0
  16. fastlife/templates/H2.jinja +4 -0
  17. fastlife/templates/Hidden.jinja +6 -0
  18. fastlife/templates/Input.jinja +8 -0
  19. fastlife/templates/Label.jinja +3 -0
  20. fastlife/templates/Option.jinja +2 -0
  21. fastlife/templates/Radio.jinja +8 -0
  22. fastlife/templates/Select.jinja +8 -0
  23. fastlife/templates/__init__.py +0 -0
  24. fastlife/templates/pydantic_form/Boolean.jinja +7 -0
  25. fastlife/templates/pydantic_form/Dropdown.jinja +17 -0
  26. fastlife/templates/pydantic_form/Hidden.jinja +2 -0
  27. fastlife/templates/pydantic_form/Model.jinja +16 -0
  28. fastlife/templates/pydantic_form/Sequence.jinja +37 -0
  29. fastlife/templates/pydantic_form/Text.jinja +12 -0
  30. fastlife/templates/pydantic_form/Union.jinja +26 -0
  31. fastlife/templates/pydantic_form/Widget.jinja +10 -0
  32. fastlife/templating/__init__.py +2 -2
  33. fastlife/templating/binding.py +6 -7
  34. fastlife/templating/renderer/__init__.py +2 -2
  35. fastlife/templating/renderer/abstract.py +9 -7
  36. fastlife/templating/renderer/jinjax.py +73 -0
  37. fastlife/templating/renderer/widgets/base.py +14 -13
  38. fastlife/templating/renderer/widgets/boolean.py +1 -1
  39. fastlife/templating/renderer/widgets/dropdown.py +7 -6
  40. fastlife/templating/renderer/widgets/factory.py +14 -7
  41. fastlife/templating/renderer/widgets/hidden.py +1 -1
  42. fastlife/templating/renderer/widgets/model.py +8 -10
  43. fastlife/templating/renderer/widgets/sequence.py +4 -4
  44. fastlife/templating/renderer/widgets/text.py +1 -1
  45. fastlife/templating/renderer/widgets/union.py +5 -4
  46. fastlife/testing/testclient.py +115 -10
  47. fastlife/views/pydantic_form.py +8 -3
  48. {fastlifeweb-0.2.3.dist-info → fastlifeweb-0.3.1.dist-info}/METADATA +3 -3
  49. fastlifeweb-0.3.1.dist-info/RECORD +63 -0
  50. {fastlifeweb-0.2.3.dist-info → fastlifeweb-0.3.1.dist-info}/WHEEL +1 -1
  51. fastlife/templates/base.jinja2 +0 -2
  52. fastlife/templates/globals.jinja2 +0 -83
  53. fastlife/templates/pydantic_form/boolean.jinja2 +0 -8
  54. fastlife/templates/pydantic_form/dropdown.jinja2 +0 -18
  55. fastlife/templates/pydantic_form/hidden.jinja2 +0 -1
  56. fastlife/templates/pydantic_form/model.jinja2 +0 -16
  57. fastlife/templates/pydantic_form/sequence.jinja2 +0 -41
  58. fastlife/templates/pydantic_form/text.jinja2 +0 -16
  59. fastlife/templates/pydantic_form/union.jinja2 +0 -40
  60. fastlife/templates/pydantic_form/widget.jinja2 +0 -10
  61. fastlife/templating/renderer/jinja2.py +0 -110
  62. fastlifeweb-0.2.3.dist-info/RECORD +0 -50
  63. {fastlifeweb-0.2.3.dist-info → fastlifeweb-0.3.1.dist-info}/LICENSE +0 -0
fastlife/__init__.py CHANGED
@@ -1,4 +1,6 @@
1
1
  from .configurator import Configurator, configure
2
+ from .configurator.registry import Registry
3
+ from .request.form_data import model
2
4
  from .templating import Template, template
3
5
 
4
6
  __all__ = [
@@ -7,4 +9,7 @@ __all__ = [
7
9
  "Configurator",
8
10
  "template",
9
11
  "Template",
12
+ "Registry",
13
+ # Model
14
+ "model",
10
15
  ]
@@ -2,8 +2,8 @@
2
2
  The configurator is here to register routes in a fastapi app,
3
3
  with dependency injection.
4
4
  """
5
- import inspect
6
5
  import importlib
6
+ import inspect
7
7
  import logging
8
8
  from enum import Enum
9
9
  from pathlib import Path
@@ -31,7 +31,7 @@ from fastlife.security.csrf import check_csrf
31
31
  from .settings import Settings
32
32
 
33
33
  if TYPE_CHECKING:
34
- from .registry import AppRegistry
34
+ from .registry import AppRegistry # coverage: ignore
35
35
 
36
36
  log = logging.getLogger(__name__)
37
37
  VENUSIAN_CATEGORY = "fastlife"
@@ -1,17 +1,21 @@
1
- from typing import Annotated
1
+ from typing import TYPE_CHECKING, Annotated
2
2
 
3
3
  from fastapi import Depends
4
4
 
5
5
  from fastlife.security.policy import CheckPermission
6
6
  from fastlife.shared_utils.resolver import resolve
7
- from fastlife.templating.renderer import AbstractTemplateRenderer
7
+
8
+ if TYPE_CHECKING:
9
+ from fastlife.templating.renderer import ( # coverage: ignore
10
+ AbstractTemplateRenderer, # coverage: ignore
11
+ ) # coverage: ignore
8
12
 
9
13
  from .settings import Settings
10
14
 
11
15
 
12
16
  class AppRegistry:
13
17
  settings: Settings
14
- renderer: AbstractTemplateRenderer
18
+ renderer: "AbstractTemplateRenderer"
15
19
  check_permission: CheckPermission
16
20
 
17
21
  def __init__(self, settings: Settings) -> None:
@@ -12,7 +12,7 @@ class Settings(BaseSettings):
12
12
  template_search_path: str = Field(default="fastlife:templates")
13
13
  registry_class: str = Field(default="fastlife.configurator.registry:AppRegistry")
14
14
  template_renderer_class: str = Field(
15
- default="fastlife.templating.renderer.jinja2:Jinja2TemplateRenderer"
15
+ default="fastlife.templating.renderer:JinjaxTemplateRenderer"
16
16
  )
17
17
  form_data_model_prefix: str = Field(default="payload")
18
18
  csrf_token_name: str = Field(default="csrf_token")
@@ -26,7 +26,6 @@ def unflatten_struct(
26
26
  ) -> Mapping[str, Any] | Sequence[Any]:
27
27
  # we sort to ensure that list index are ordered
28
28
  formkeys = sorted(flatten_input.keys())
29
-
30
29
  for key in formkeys:
31
30
  if csrf_token_name is not None and key == csrf_token_name:
32
31
  continue
@@ -39,15 +38,11 @@ def unflatten_struct(
39
38
  raise ValueError(f"{flatten_input}: Not a list")
40
39
  while int(key) != len(unflattened_output):
41
40
  unflattened_output.append(None)
42
- if not int(key) == len(unflattened_output):
43
- raise ValueError(
44
- f"{flatten_input}: Missing index {len(unflattened_output)}"
45
- )
46
41
  unflattened_output.append(flatten_input[key])
47
42
  elif isinstance(unflattened_output, dict):
48
43
  unflattened_output[key] = flatten_input[key]
49
44
  else:
50
- raise ValueError(type(unflattened_output))
45
+ raise TypeError(type(unflattened_output))
51
46
  continue
52
47
 
53
48
  child_is_list = rest.partition(".")[0].isdigit()
@@ -74,7 +69,7 @@ def unflatten_struct(
74
69
  level + 1,
75
70
  )
76
71
  else:
77
- raise ValueError(type(unflattened_output))
72
+ raise TypeError(type(unflattened_output))
78
73
 
79
74
  return unflattened_output
80
75
 
fastlife/security/csrf.py CHANGED
@@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Any, Callable, Coroutine
4
4
  from fastapi import Request
5
5
 
6
6
  if TYPE_CHECKING:
7
- from fastlife.configurator.registry import Registry
7
+ from fastlife.configurator.registry import Registry # coverage: ignore
8
8
 
9
9
 
10
10
  class CSRFAttack(Exception):
@@ -39,7 +39,6 @@ class SessionMiddleware(AbstractMiddleware):
39
39
  if scope["type"] not in ("http", "websocket"): # pragma: no cover
40
40
  await self.app(scope, receive, send)
41
41
  return
42
-
43
42
  connection = HTTPConnection(scope)
44
43
  existing_session = self.cookie_name in connection.cookies
45
44
  if existing_session:
File without changes
@@ -8,16 +8,18 @@ def resolve(value: str) -> Any:
8
8
  module_name, attr_name = value.split(":", 2)
9
9
  spec = importlib.util.find_spec(module_name)
10
10
  if spec is None:
11
- raise ValueError(f"Module {module_name} not found.")
11
+ raise ValueError(f"Module {module_name} not found")
12
12
  module = importlib.util.module_from_spec(spec)
13
13
  if spec.loader is None:
14
- raise ValueError("No loader on spec")
14
+ # I can't figure out when the spec.loader is None,
15
+ # just relying on typing here
16
+ raise ValueError("No loader on spec") # coverage: ignore
15
17
  spec.loader.exec_module(module)
16
18
 
17
19
  try:
18
20
  attr = getattr(module, attr_name)
19
21
  except AttributeError:
20
- raise ValueError(f"Attribute {attr_name} not found in module {module_name}.")
22
+ raise ValueError(f"Attribute {attr_name} not found in module {module_name}")
21
23
 
22
24
  return attr
23
25
 
@@ -0,0 +1,5 @@
1
+ {# def href, target="#maincontent", swap="innerHTML show:body:top", push_url=true #}
2
+ <a href="{{href}}" hx-get="{{href}}" hx-target="{{target}}" hx-swap="{{swap}}" {% if push_url %}hx-push-url="true" {%- endif %}
3
+ class="text-neutral-900 bg-neutral-200 hover:bg-neutral-50 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-neutral-400 dark:hover:bg-neutral-300 dark:focus:ring-neutral-100">
4
+ {{ content }}
5
+ </a>
@@ -0,0 +1,27 @@
1
+ {# def
2
+ type="submit",
3
+ id="",
4
+ name="action",
5
+ value="submit",
6
+ onclick="",
7
+ target="",
8
+ swap="",
9
+ get="",
10
+ select="",
11
+ after_request="",
12
+ vals="",
13
+ full_width=false,
14
+ hidden=false,
15
+ aria_label=""
16
+ #}
17
+ {% set css = "text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300
18
+ font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700
19
+ dark:focus:ring-primary-800" %}
20
+ <button type="{{type}}" {%if id %}id="{{id}}" {%endif%} name="{{name}}" value="{{value}}" {% if
21
+ target%}hx-target="{{target}}" {% endif %} {% if swap %}hx-swap="{{swap}}" {% endif %} {% if select
22
+ %}hx-select="{{select}}" {% endif %} {% if get %}hx-get="{{get}}" {% endif %} {% if onclick %}onclick="{{onclick}}" {%
23
+ endif %}{% if after_request %}hx-on::after-request="{{after_request}}" {% endif %} {% if vals
24
+ %}hx-vals='{{vals|safe}}' {% endif %} {% if aria_label %}aria-label="{{aria_label}}" {% endif %}
25
+ class="{% if full_width %}w-full {% endif %}{{css}}" {% if hidden %}hidden{% endif %}>
26
+ {{ content}}
27
+ </button>
@@ -0,0 +1,2 @@
1
+ {# def name, value, id #}
2
+ <input name="{{name}}" value="{{value}}" type="checkbox" id="{{id}}" />
@@ -0,0 +1,2 @@
1
+ {# def csrf_token #}
2
+ <Hidden name={csrf_token.name} value={csrf_token.value} />
@@ -0,0 +1,5 @@
1
+ {# def csrf_token #}
2
+ <form class="space-y-4 md:space-y-6" hx-post="" method="post">
3
+ <CsrfToken csrf_token={csrf_token}/>
4
+ {{ content }}
5
+ </form>
@@ -0,0 +1,4 @@
1
+ <h1
2
+ class="block pb-4 font-sans text-5xl font-bold leading-tight tracking-tight text-neutral-900 md:text-4xl dark:text-white">
3
+ {{ content }}
4
+ </h1>
@@ -0,0 +1,4 @@
1
+ <h2
2
+ class="block pb-4 font-sans text-4xl font-bold leading-tight tracking-tight text-neutral-900 md:text-4xl dark:text-white">
3
+ {{ content }}
4
+ </h2>
@@ -0,0 +1,6 @@
1
+ {# def
2
+ name,
3
+ value,
4
+ id="",
5
+ #}
6
+ <input name="{{name}}" value="{{value}}" type="hidden" {%if id %}id="{{id}}" {%endif%} />
@@ -0,0 +1,8 @@
1
+ {# def name, value="", input_type="text", id="", aria_label="", placeholder="" #}
2
+ {% set css = "bg-neutral-50 border border-neutral-300 text-neutral-900 text-base rounded-lg focus:ring-primary-500
3
+ focus:border-primary-500 block w-full p-2.5 dark:bg-neutral-700 dark:border-neutral-600 dark:placeholder-neutral-400
4
+ dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" %}
5
+
6
+ <input name="{{name}}" value="{{value}}" type="{{input_type}}" id="{{id}}" {% if aria_label
7
+ %}aria-label="{{aria_label}}" {% endif %} {% if placeholder %}placeholder="{{placeholder}}" {% endif %}
8
+ class="{{css}}" />
@@ -0,0 +1,3 @@
1
+ {%- set css ="block mb-2 text-base font-bold text-neutral-900 dark:text-white" -%}
2
+
3
+ <label for="{{attrs.for}}" class="{{css}}">{{content}}</label>
@@ -0,0 +1,2 @@
1
+ {# def value #}
2
+ <option value="{{value}}">{{ content }}</option>
@@ -0,0 +1,8 @@
1
+ {# def label name id value checked=false disabled=false onclick="" #}
2
+ <div class="flex items-center mb-4">
3
+ <input type="radio" name="{{name}}" id="{{id}}" {% if value %}value="{{value}}" {% endif %}{% if checked %}checked{%
4
+ endif %}
5
+ class="w-4 h-4 text-primary-600 bg-neutral-100 border-neutral-300 focus:ring-primary-500 dark:focus:ring-primary-600 dark:ring-offset-neutral-800 focus:ring-2 dark:bg-neutral-700 dark:border-neutral-600"
6
+ {% if onclick %}onclick="{{onclick}}" {% endif %} {%if disabled%}disabled{%endif%}>
7
+ <label for="{{id}}" class="ms-2 text-sm font-medium text-neutral-900 dark:text-neutral-300">{{label}}</label>
8
+ </div>
@@ -0,0 +1,8 @@
1
+ {# def name, id #}
2
+ {% set css = "bg-neutral-50 border border-neutral-300 text-neutral-900 text-base rounded-lg focus:ring-primary-500
3
+ focus:border-primary-500 block w-full p-2.5 dark:bg-neutral-700 dark:border-neutral-600 dark:placeholder-neutral-400
4
+ dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" %}
5
+
6
+ <select name="{{name}}" id="{{id}}" class="{{css}}">
7
+ {{content}}
8
+ </select>
File without changes
@@ -0,0 +1,7 @@
1
+ {# def widget #}
2
+ <pydantic_form.Widget widget={widget} removable={widget.removable}>
3
+ <div class="pt-4">
4
+ <Label for={widget.id}>{{widget.title}}</Label>
5
+ <Checkbox name={widget.name} value={widget.value} type="checkbox" id={widget.id} />
6
+ </div>
7
+ </pydantic_form.Widget>
@@ -0,0 +1,17 @@
1
+ {# def widget #}
2
+
3
+ <pydantic_form.Widget widget={widget} removable={widget.removable}>
4
+ <div class="pt-4">
5
+ <Label for={widget.id}>{{widget.title}}</Label>
6
+ <Select name={widget.name} id={widget.id}>
7
+ {%- for opt in widget.options -%}
8
+ <Option value={opt.value}>
9
+ {{ opt.text }}
10
+ </Option>
11
+ {%- endfor -%}
12
+ </Select>
13
+ {%- if widget.help_text -%}
14
+ <span class="mt-2 text-sm text-neutral-500 dark:text-neutral-400">{{widget.help_text}}</span>
15
+ {%- endif %}
16
+ </div>
17
+ </pydantic_form.Widget>
@@ -0,0 +1,2 @@
1
+ {# def widget #}
2
+ <Hidden name={widget.name} value={widget.value} id={widget.id} />
@@ -0,0 +1,16 @@
1
+ {# def widget, children_widget #}
2
+
3
+ <pydantic_form.Widget widget={widget} removable={widget.removable}>
4
+ <div id="{{widget.id}}" class="m-4">
5
+ <details open>
6
+ <summary class="justify-between items-center font-medium cursor-pointer">
7
+ <h4 class="inline font-sans text-3xl font-bold">{{widget.title}}</h4>
8
+ </summary>
9
+ <div>
10
+ {% for child in children_widget %}
11
+ {{ child }}
12
+ {% endfor %}
13
+ </div>
14
+ </details>
15
+ </div>
16
+ </pydantic_form.Widget>
@@ -0,0 +1,37 @@
1
+ {# def widget, children_widgets, type #}
2
+
3
+ <pydantic_form.Widget widget={widget} removable={widget.removable}>
4
+ <details id="{{widget.id}}" open>
5
+ <summary class="justify-between items-center font-medium cursor-pointer">
6
+ <h4 class="inline font-sans text-3xl font-bold">{{widget.title}}</h4>
7
+ </summary>
8
+ <div>
9
+ {% set fnGetName = "get" + widget.id.replace("-", "_") %}
10
+ <script>
11
+ function {{fnGetName}}() {
12
+ const el = document.getElementById("{{widget.id}}-content");
13
+ const len = el.dataset.length;
14
+ el.dataset.length = parseInt(len) + 1;
15
+ return "{{type.name}}." + len;
16
+ }
17
+ </script>
18
+
19
+ <div id="{{widget.id}}-content" class="m-4" data-length="{{children_widgets|length|string}}">
20
+ {% for child in children_widgets %}
21
+ {% set container_id = widget.id + "-container" %}
22
+ <div id="{{container_id}}">
23
+ {{ child }}
24
+ </div>
25
+ {% endfor%}
26
+ </div>
27
+ <div>
28
+ {% set container_id = "#" + widget.id + "-container" %}
29
+ {% set add_id = widget.id + "-add" %}
30
+ {% set vals = 'js:{"name": '+ fnGetName + '(), "token": "' + type.token + '", "removable": true}' %}
31
+ <Button type="button" target={container_id} swap="beforeend" id={add_id} vals={vals} get={type.url}>
32
+ Add
33
+ </Button>
34
+ </div>
35
+ </div>
36
+ </details>
37
+ </pydantic_form.Widget>
@@ -0,0 +1,12 @@
1
+ {# def widget #}
2
+
3
+ <pydantic_form.Widget widget={widget} removable={widget.removable}>
4
+ <div class="pt-4">
5
+ <Label for={widget.id}>{{widget.title}}</Label>
6
+ <Input name={widget.name} value={widget.value} type={widget.input_type} id={widget.id}
7
+ aria-label={widget.aria_label} placeholder={widget.placeholder} />
8
+ {%- if widget.help_text -%}
9
+ <span class="mt-2 text-sm text-neutral-500 dark:text-neutral-400">{{widget.help_text}}</span>
10
+ {%- endif %}
11
+ </div>
12
+ </pydantic_form.Widget>
@@ -0,0 +1,26 @@
1
+ {# def widget, child, types, parent_type #}
2
+
3
+ <pydantic_form.Widget widget={widget}>
4
+ <div id="{{widget.id}}">
5
+ <details open>
6
+ <summary class="justify-between items-center font-medium cursor-pointer">
7
+ <h4 class="inline font-sans text-3xl font-bold">{{widget.title}}</h4>
8
+ </summary>
9
+ <div hx-sync="this" id="{{widget.id}}-child">
10
+ {% if child %}
11
+ {{ child }}
12
+ {% else %}
13
+ {% for typ in types %}
14
+ <Button type="button" target="closest div" get={typ.url} vals={typ.params|tojson}
15
+ id={typ.fullname + "-" + widget.token}
16
+ onclick={"document.getElementById('" + widget.id +"-remove-btn').hidden=false"}>{{typ.title}}</Button>
17
+ {% endfor %}
18
+ {% endif %}
19
+ </div>
20
+ <Button type="button" id={widget.id + "-remove-btn" } target={"#" + widget.id} vals={parent_type.params|tojson}
21
+ get={parent_type.url} hidden={not child}>
22
+ Remove
23
+ </Button>
24
+ </details>
25
+ </div>
26
+ </pydantic_form.Widget>
@@ -0,0 +1,10 @@
1
+ {# def widget, removable=false #}
2
+ {% set container_id = widget.id + "-container" %}
3
+ <div id="{{container_id}}">
4
+ {{ content }}
5
+ {% if removable %}
6
+ <Button type="button" onclick={"document.getElementById('" + container_id + "').remove()"}>
7
+ Remove
8
+ </Button>
9
+ {% endif %}
10
+ </div>
@@ -1,9 +1,9 @@
1
1
  from .binding import Template, template
2
- from .renderer import AbstractTemplateRenderer, Jinja2TemplateRenderer
2
+ from .renderer import AbstractTemplateRenderer, JinjaxTemplateRenderer
3
3
 
4
4
  __all__ = [
5
5
  "AbstractTemplateRenderer",
6
- "Jinja2TemplateRenderer",
6
+ "JinjaxTemplateRenderer",
7
7
  "Template",
8
8
  "template",
9
9
  ]
@@ -1,12 +1,11 @@
1
- from typing import Any, Callable, Coroutine
1
+ from typing import Any, Callable
2
2
 
3
3
  from fastapi import Depends, Request, Response
4
4
 
5
5
  from fastlife.configurator.registry import Registry
6
6
  from fastlife.security.csrf import create_csrf_token
7
7
 
8
- TemplateEngineHandler = Coroutine[Any, Any, Response]
9
- Template = Callable[..., TemplateEngineHandler]
8
+ Template = Callable[..., Response]
10
9
  TemplateEngine = Callable[["Registry", Request], Template]
11
10
 
12
11
 
@@ -19,12 +18,12 @@ def get_page_template(
19
18
  *,
20
19
  _create_csrf_token: Callable[..., str] = create_csrf_token,
21
20
  ) -> Template:
22
- async def parametrizer(**kwargs: Any) -> Response:
21
+ def parametrizer(**kwargs: Any) -> Response:
23
22
  request.scope[reg.settings.csrf_token_name] = (
24
23
  request.cookies.get(reg.settings.csrf_token_name)
25
24
  or _create_csrf_token()
26
25
  )
27
- data = await reg.renderer.render_page(request, template, **kwargs)
26
+ data = reg.renderer.render_page(request, template, **kwargs)
28
27
  resp = Response(data, headers={"Content-Type": content_type})
29
28
  resp.set_cookie(
30
29
  reg.settings.csrf_token_name,
@@ -44,8 +43,8 @@ def get_template(
44
43
  template: str, *, content_type: str = "text/html"
45
44
  ) -> Callable[["Registry"], Template]:
46
45
  def render_template(reg: "Registry") -> Template:
47
- async def parametrizer(**kwargs: Any) -> Response:
48
- data = await reg.renderer.render_template(template, **kwargs)
46
+ def parametrizer(**kwargs: Any) -> Response:
47
+ data = reg.renderer.render_template(template, **kwargs)
49
48
  resp = Response(data, headers={"Content-Type": content_type})
50
49
  return resp
51
50
 
@@ -1,4 +1,4 @@
1
1
  from .abstract import AbstractTemplateRenderer
2
- from .jinja2 import Jinja2TemplateRenderer
2
+ from .jinjax import JinjaxTemplateRenderer
3
3
 
4
- __all__ = ["AbstractTemplateRenderer", "Jinja2TemplateRenderer"]
4
+ __all__ = ["AbstractTemplateRenderer", "JinjaxTemplateRenderer"]
@@ -1,8 +1,9 @@
1
1
  import abc
2
- from typing import Any, Mapping, Optional, Type
2
+ from typing import Any, Mapping, Type
3
3
 
4
4
  from fastapi import Request
5
5
  from markupsafe import Markup
6
+ from pydantic.fields import FieldInfo
6
7
 
7
8
 
8
9
  class AbstractTemplateRenderer(abc.ABC):
@@ -10,20 +11,21 @@ class AbstractTemplateRenderer(abc.ABC):
10
11
  """Used to prefix url to fetch fast life widgets."""
11
12
 
12
13
  @abc.abstractmethod
13
- async def render_page(self, request: Request, template: str, **params: Any) -> str:
14
+ def render_page(self, request: Request, template: str, **params: Any) -> str:
14
15
  ...
15
16
 
16
17
  @abc.abstractmethod
17
- async def render_template(self, template: str, **params: Any) -> str:
18
+ def render_template(self, template: str, **params: Any) -> str:
18
19
  ...
19
20
 
20
21
  @abc.abstractmethod
21
- async def pydantic_form(
22
+ def pydantic_form(
22
23
  self,
23
24
  model: Type[Any],
24
- form_data: Optional[Mapping[str, Any]] = None,
25
- name: Optional[str] = None,
26
- token: Optional[str] = None,
25
+ form_data: Mapping[str, Any] | None = None,
26
+ name: str | None = None,
27
+ token: str | None = None,
27
28
  removable: bool = False,
29
+ field: FieldInfo | None = None,
28
30
  ) -> Markup:
29
31
  ...
@@ -0,0 +1,73 @@
1
+ from typing import TYPE_CHECKING, Any, Mapping, Optional, Sequence, Type
2
+
3
+ from fastapi import Request
4
+ from jinjax.catalog import Catalog
5
+ from markupsafe import Markup
6
+ from pydantic.fields import FieldInfo
7
+
8
+ from fastlife.templating.renderer.widgets.factory import WidgetFactory
9
+
10
+ if TYPE_CHECKING:
11
+ from fastlife.configurator.settings import Settings # coverage: ignore
12
+
13
+ from fastlife.shared_utils.resolver import resolve_path
14
+
15
+ from .abstract import AbstractTemplateRenderer
16
+
17
+
18
+ def build_searchpath(template_search_path: str) -> Sequence[str]:
19
+ searchpath: list[str] = []
20
+ paths = template_search_path.split(",")
21
+
22
+ for path in paths:
23
+ if ":" in path:
24
+ searchpath.append(resolve_path(path))
25
+ else:
26
+ searchpath.append(path)
27
+ return searchpath
28
+
29
+
30
+ class JinjaxTemplateRenderer(AbstractTemplateRenderer):
31
+ route_prefix: str
32
+ """Used to prefix url to fetch fast life widgets."""
33
+
34
+ def __init__(self, settings: "Settings") -> None:
35
+ self.route_prefix = settings.fastlife_route_prefix
36
+ self.form_data_model_prefix = settings.form_data_model_prefix
37
+ self.csrf_token_name = settings.csrf_token_name
38
+
39
+ self.catalog = Catalog()
40
+ for path in build_searchpath(settings.template_search_path):
41
+ self.catalog.add_folder(path)
42
+
43
+ def render_page(self, request: Request, template: str, **params: Any) -> str:
44
+ return self.catalog.render( # type:ignore
45
+ template,
46
+ request=request,
47
+ csrf_token={
48
+ "name": self.csrf_token_name,
49
+ "value": request.scope.get(self.csrf_token_name, ""),
50
+ },
51
+ pydantic_form=self.pydantic_form,
52
+ **params
53
+ )
54
+
55
+ def render_template(self, template: str, **params: Any) -> str:
56
+ return self.catalog.render(template, **params) # type:ignore
57
+
58
+ def pydantic_form(
59
+ self,
60
+ model: Type[Any],
61
+ form_data: Optional[Mapping[str, Any]] = None,
62
+ name: Optional[str] = None,
63
+ token: Optional[str] = None,
64
+ removable: bool = False,
65
+ field: FieldInfo | None = None,
66
+ ) -> Markup:
67
+ return WidgetFactory(self, token).get_markup(
68
+ model,
69
+ form_data or {},
70
+ prefix=(name or self.form_data_model_prefix),
71
+ removable=removable,
72
+ field=field,
73
+ )
@@ -42,18 +42,15 @@ class Widget(abc.ABC):
42
42
  self.aria_label = aria_label or ""
43
43
  self.token = token or secrets.token_urlsafe(4).replace("_", "-")
44
44
  self.removable = removable
45
-
46
- @property
47
- def id(self) -> str:
48
- return f"{self.name}-{self.token}".replace("_", "-").replace(".", "-")
45
+ self.id = f"{self.name}-{self.token}".replace("_", "-").replace(".", "-")
49
46
 
50
47
  @abc.abstractmethod
51
48
  def get_template(self) -> str:
52
49
  ...
53
50
 
54
- async def to_html(self, renderer: AbstractTemplateRenderer) -> Markup:
51
+ def to_html(self, renderer: AbstractTemplateRenderer) -> Markup:
55
52
  """Return the html version"""
56
- return Markup(await renderer.render_template(self.get_template(), widget=self))
53
+ return Markup(renderer.render_template(self.get_template(), widget=self))
57
54
 
58
55
 
59
56
  def _get_fullname(typ: Type[Any]) -> str:
@@ -64,11 +61,18 @@ def _get_fullname(typ: Type[Any]) -> str:
64
61
 
65
62
 
66
63
  class TypeWrapper:
67
- def __init__(self, typ: Type[Any], route_prefix: str, name: str, token: str):
64
+ def __init__(
65
+ self,
66
+ typ: Type[Any],
67
+ route_prefix: str,
68
+ name: str,
69
+ token: str,
70
+ title: str | None = None,
71
+ ):
68
72
  self.typ = typ
69
73
  self.route_prefix = route_prefix
70
74
  self.name = name
71
- self.title = get_title(typ)
75
+ self.title = title or get_title(typ)
72
76
  self.token = token
73
77
 
74
78
  @property
@@ -77,12 +81,9 @@ class TypeWrapper:
77
81
 
78
82
  @property
79
83
  def params(self) -> Mapping[str, str]:
80
- return {"name": self.name, "token": self.token}
84
+ return {"name": self.name, "token": self.token, "title": self.title}
81
85
 
82
86
  @property
83
87
  def url(self) -> str:
84
- ret = (
85
- f"{self.route_prefix}/pydantic-form/widgets/{self.fullname}"
86
- # f"?name={self.name}&token={self.token}"
87
- )
88
+ ret = f"{self.route_prefix}/pydantic-form/widgets/{self.fullname}"
88
89
  return ret
@@ -17,4 +17,4 @@ class BooleanWidget(Widget):
17
17
  self.value = value
18
18
 
19
19
  def get_template(self) -> str:
20
- return "pydantic_form/boolean.jinja2"
20
+ return "pydantic_form.Boolean"