|
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +import dataclasses |
| 4 | +import functools |
| 5 | +import json |
| 6 | +import typing |
| 7 | + |
| 8 | +import chalicelib.aws_resource as aws_resource |
| 9 | +import chalicelib.template_manager.__interface__ as template_mgr_interface |
| 10 | +import chalicelib.util.type_util as type_util |
| 11 | +import jinja2 |
| 12 | +import jinja2.meta |
| 13 | +import jinja2.nodes |
| 14 | +import pydantic |
| 15 | + |
| 16 | + |
| 17 | +class TemplateInformation(pydantic.BaseModel): |
| 18 | + code: str |
| 19 | + template: type_util.TemplateType |
| 20 | + |
| 21 | + @pydantic.computed_field # type: ignore[misc] |
| 22 | + @property |
| 23 | + def template_variables(self) -> set[str]: |
| 24 | + # From https://stackoverflow.com/a/77363330 |
| 25 | + template = jinja2.Environment(autoescape=True).parse(source=json.dumps(self.template)) |
| 26 | + return jinja2.meta.find_undeclared_variables(ast=template) |
| 27 | + |
| 28 | + |
| 29 | +TemplateStructureType = typing.TypeVar("TemplateStructureType", bound=pydantic.BaseModel) |
| 30 | + |
| 31 | + |
| 32 | +class TemplateManagerInterface(typing.Protocol[TemplateStructureType]): |
| 33 | + template_structure_cls: type[TemplateStructureType] | type[str] | None = None |
| 34 | + |
| 35 | + def check_template_valid(self, template_data: type_util.TemplateType) -> bool: |
| 36 | + if not self.template_structure_cls or self.template_structure_cls == str: |
| 37 | + return True |
| 38 | + typing.cast(type[TemplateStructureType], self.template_structure_cls).model_validate(template_data) |
| 39 | + return True |
| 40 | + |
| 41 | + def list(self) -> list[TemplateInformation]: ... |
| 42 | + |
| 43 | + def retrieve(self, code: str) -> TemplateInformation | None: ... |
| 44 | + |
| 45 | + def create(self, code: str, template_data: type_util.TemplateType) -> TemplateInformation: ... |
| 46 | + |
| 47 | + def update(self, code: str, template_data: type_util.TemplateType) -> TemplateInformation: ... |
| 48 | + |
| 49 | + def delete(self, code: str) -> None: ... |
| 50 | + |
| 51 | + def render(self, code: str, context: type_util.ContextType) -> type_util.TemplateType: ... |
| 52 | + |
| 53 | + |
| 54 | +S3TemplateStructureType = typing.TypeVar("S3TemplateStructureType", bound=pydantic.BaseModel) |
| 55 | + |
| 56 | + |
| 57 | +@dataclasses.dataclass |
| 58 | +class S3ResourceTemplateManager(template_mgr_interface.TemplateManagerInterface): |
| 59 | + resource: typing.ClassVar[aws_resource.S3ResourcePath] |
| 60 | + |
| 61 | + def list(self) -> list[template_mgr_interface.TemplateInformation]: |
| 62 | + return [self.retrieve(code=f.split(sep=".")[0]) for f in self.resource.list_objects(filter_by_extension=True)] |
| 63 | + |
| 64 | + @functools.lru_cache # noqa: B019 |
| 65 | + def retrieve(self, code: str) -> template_mgr_interface.TemplateInformation: |
| 66 | + template_body: str = self.resource.download(code=code).decode(encoding="utf-8") |
| 67 | + return template_mgr_interface.TemplateInformation(code=code, template=template_body) |
| 68 | + |
| 69 | + def create(self, code: str, template_data: type_util.TemplateType) -> template_mgr_interface.TemplateInformation: |
| 70 | + self.check_template_valid(template_data=template_data) |
| 71 | + self.resource.upload(code=code, content=template_data) |
| 72 | + return template_mgr_interface.TemplateInformation(code=code, template=template_data) |
| 73 | + |
| 74 | + def update(self, code: str, template_data: type_util.TemplateType) -> template_mgr_interface.TemplateInformation: |
| 75 | + return self.create(code=code, template_data=template_data) |
| 76 | + |
| 77 | + def delete(self, code: str) -> None: |
| 78 | + self.resource.delete(code=code) |
| 79 | + |
| 80 | + def render(self, code: str, context: type_util.ContextType) -> type_util.TemplateType: |
| 81 | + return json.loads(jinja2.Template(source=json.dumps(obj=self.retrieve(code=code).template)).render(context)) |
0 commit comments