htmy 0.2.0__tar.gz → 0.3.0__tar.gz

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.

Potentially problematic release.


This version of htmy might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: htmy
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Async, pure-Python rendering engine.
5
5
  License: MIT
6
6
  Author: Peter Volf
@@ -12,6 +12,7 @@ Classifier: Programming Language :: Python :: 3.11
12
12
  Classifier: Programming Language :: Python :: 3.12
13
13
  Classifier: Programming Language :: Python :: 3.13
14
14
  Requires-Dist: anyio (>=4.6.2.post1,<5.0.0)
15
+ Requires-Dist: async-lru (>=2.0.4,<3.0.0)
15
16
  Requires-Dist: markdown (>=3.7,<4.0)
16
17
  Description-Content-Type: text/markdown
17
18
 
@@ -34,6 +35,8 @@ Description-Content-Type: text/markdown
34
35
  - **Powerful**, React-like **context support**, so you can avoid prop-drilling.
35
36
  - Sync and async **function components** with **decorator syntax**.
36
37
  - All baseline **HTML** tags built-in.
38
+ - **Markdown** support with tools for customization.
39
+ - Async, JSON based **internationalization**.
37
40
  - Built-in, easy to use `ErrorBoundary` component for graceful error handling.
38
41
  - **Unopinionated**: use the backend, CSS, and JS frameworks of your choice, the way you want to use them.
39
42
  - Everything is **easily customizable**, from the rendering engine to components, formatting and context management.
@@ -157,10 +160,11 @@ user_table = html.table(
157
160
  `htmy` has a rich set of built-in utilities and components for both HTML and other use-cases:
158
161
 
159
162
  - `html` module: a complete set of [baseline HTML tags](https://developer.mozilla.org/en-US/docs/Glossary/Baseline/Compatibility).
163
+ - `md`: `MarkdownParser` utility and `MD` component for loading, parsing, converting, and rendering markdown content.
164
+ - `i18n`: utilities for async, JSON based internationalization.
160
165
  - `BaseTag`, `TagWithProps`, `Tag`, `WildcardTag`: base classes for custom XML tags.
161
166
  - `ErrorBoundary`, `Fragment`, `SafeStr`, `WithContext`: utilities for error handling, component wrappers, context providers, and formatting.
162
167
  - `Snippet`: utility class for loading and customizing document snippets from the file system.
163
- - `md`: `MarkdownParser` utility and `MD` component for loading, parsing, converting, and rendering markdown content.
164
168
  - `etree.ETreeConverter`: utility that converts XML to a component tree with support for custom HTMY components.
165
169
 
166
170
  ### Rendering
@@ -307,6 +311,7 @@ The primary aim of `htmy` is to be an **async**, pure-Python rendering engine, w
307
311
  The library aims to minimze its dependencies. Currently the following dependencies are required:
308
312
 
309
313
  - `anyio`: for async file operations and networking.
314
+ - `async-lru`: for async caching.
310
315
  - `markdown`: for markdown parsing.
311
316
 
312
317
  ## Development
@@ -17,6 +17,8 @@
17
17
  - **Powerful**, React-like **context support**, so you can avoid prop-drilling.
18
18
  - Sync and async **function components** with **decorator syntax**.
19
19
  - All baseline **HTML** tags built-in.
20
+ - **Markdown** support with tools for customization.
21
+ - Async, JSON based **internationalization**.
20
22
  - Built-in, easy to use `ErrorBoundary` component for graceful error handling.
21
23
  - **Unopinionated**: use the backend, CSS, and JS frameworks of your choice, the way you want to use them.
22
24
  - Everything is **easily customizable**, from the rendering engine to components, formatting and context management.
@@ -140,10 +142,11 @@ user_table = html.table(
140
142
  `htmy` has a rich set of built-in utilities and components for both HTML and other use-cases:
141
143
 
142
144
  - `html` module: a complete set of [baseline HTML tags](https://developer.mozilla.org/en-US/docs/Glossary/Baseline/Compatibility).
145
+ - `md`: `MarkdownParser` utility and `MD` component for loading, parsing, converting, and rendering markdown content.
146
+ - `i18n`: utilities for async, JSON based internationalization.
143
147
  - `BaseTag`, `TagWithProps`, `Tag`, `WildcardTag`: base classes for custom XML tags.
144
148
  - `ErrorBoundary`, `Fragment`, `SafeStr`, `WithContext`: utilities for error handling, component wrappers, context providers, and formatting.
145
149
  - `Snippet`: utility class for loading and customizing document snippets from the file system.
146
- - `md`: `MarkdownParser` utility and `MD` component for loading, parsing, converting, and rendering markdown content.
147
150
  - `etree.ETreeConverter`: utility that converts XML to a component tree with support for custom HTMY components.
148
151
 
149
152
  ### Rendering
@@ -290,6 +293,7 @@ The primary aim of `htmy` is to be an **async**, pure-Python rendering engine, w
290
293
  The library aims to minimze its dependencies. Currently the following dependencies are required:
291
294
 
292
295
  - `anyio`: for async file operations and networking.
296
+ - `async-lru`: for async caching.
293
297
  - `markdown`: for markdown parsing.
294
298
 
295
299
  ## Development
@@ -0,0 +1,165 @@
1
+ import json
2
+ from collections.abc import Mapping
3
+ from pathlib import Path
4
+ from typing import Any, ClassVar, overload
5
+
6
+ from async_lru import alru_cache
7
+
8
+ from .core import ContextAware
9
+ from .io import open_file
10
+
11
+ TranslationResource = Mapping[str, Any]
12
+ """Translation resource type."""
13
+
14
+
15
+ class I18nError(Exception): ...
16
+
17
+
18
+ class I18nKeyError(I18nError): ...
19
+
20
+
21
+ class I18nValueError(I18nError): ...
22
+
23
+
24
+ class I18n(ContextAware):
25
+ """
26
+ Context-aware async internationalization utility.
27
+ """
28
+
29
+ __slots__ = ("_path", "_fallback")
30
+
31
+ _root_keys: ClassVar[frozenset[str]] = frozenset(("", "."))
32
+ """Special keys that represent the "root" object, i.e. the entire translation resource file."""
33
+
34
+ def __init__(self, path: str | Path, fallback: str | Path | None = None) -> None:
35
+ """
36
+ Initialization.
37
+
38
+ Arguments:
39
+ path: Path to the root directory that contains the translation resources.
40
+ fallback: Optional fallback path to use if `path` doesn't contain the required resources.
41
+ """
42
+ self._path: Path = Path(path) if isinstance(path, str) else path
43
+ self._fallback: Path | None = Path(fallback) if isinstance(fallback, str) else fallback
44
+
45
+ @overload
46
+ async def get(self, dotted_path: str, key: str) -> Any: ...
47
+
48
+ @overload
49
+ async def get(self, dotted_path: str, key: str, **kwargs: Any) -> str: ...
50
+
51
+ async def get(self, dotted_path: str, key: str, **kwargs: Any) -> Any:
52
+ """
53
+ Returns the translation resource at the given location.
54
+
55
+ If keyword arguments are provided, it's expected that the referenced data
56
+ is a [format string](https://docs.python.org/3/library/string.html#formatstrings)
57
+ which can be fully formatted using the given keyword arguments.
58
+
59
+ Arguments:
60
+ dotted_path: A package-like (dot separated) path to the file that contains
61
+ the required translation resource, relative to `path`.
62
+ key: The key in the translation resource whose value is requested. Use dots to reference
63
+ embedded attributes.
64
+
65
+ Returns:
66
+ The loaded value.
67
+
68
+ Raises:
69
+ I18nError: If the given translation resource is not found or invalid.
70
+ """
71
+ try:
72
+ return await self._resolve(self._path, dotted_path, key, **kwargs)
73
+ except I18nError:
74
+ if self._fallback is None:
75
+ raise
76
+
77
+ return await self._resolve(self._fallback, dotted_path, key, **kwargs)
78
+
79
+ @classmethod
80
+ async def _resolve(cls, root: Path, dotted_subpath: str, key: str, **kwargs: Any) -> Any:
81
+ """
82
+ Resolves the given translation resource.
83
+
84
+ Arguments:
85
+ root: The root path to use.
86
+ dotted_subpath: Subpath under `root` with dots as separators.
87
+ key: The key in the translation resource.
88
+
89
+ Returns:
90
+ The resolved translation resource.
91
+
92
+ Raises:
93
+ I18nKeyError: If the translation resource doesn't contain the requested key.
94
+ I18nValueError: If the translation resource is not found or its content is invalid.
95
+ """
96
+ result = await load_translation_resource(resolve_json_path(root, dotted_subpath))
97
+ if key in cls._root_keys:
98
+ return result
99
+
100
+ for k in key.split("."):
101
+ try:
102
+ result = result[k]
103
+ except KeyError as e:
104
+ raise I18nKeyError(f"Key not found: {key}") from e
105
+
106
+ if len(kwargs) > 0:
107
+ if not isinstance(result, str):
108
+ raise I18nValueError("Formatting is only supported for strings.")
109
+
110
+ return result.format(**kwargs)
111
+
112
+ return result
113
+
114
+
115
+ @alru_cache(8)
116
+ async def load_translation_resource(path: Path) -> TranslationResource:
117
+ """
118
+ Loads the translation resource from the given path.
119
+
120
+ Arguments:
121
+ path: The path of the translation resource to load.
122
+
123
+ Returns:
124
+ The loaded translation resource.
125
+
126
+ Raises:
127
+ I18nValueError: If the translation resource is not a JSON dict.
128
+ """
129
+
130
+ try:
131
+ async with await open_file(path, "r") as f:
132
+ content = await f.read()
133
+ except FileNotFoundError as e:
134
+ raise I18nValueError(f"Translation resource not found: {str(path)}") from e
135
+
136
+ try:
137
+ result = json.loads(content)
138
+ except json.JSONDecodeError as e:
139
+ raise I18nValueError("Translation resource decoding failed.") from e
140
+
141
+ if isinstance(result, dict):
142
+ return result
143
+
144
+ raise I18nValueError("Only dict translation resources are allowed.")
145
+
146
+
147
+ def resolve_json_path(root: Path, dotted_subpath: str) -> Path:
148
+ """
149
+ Resolves the given dotted subpath relative to root.
150
+
151
+ Arguments:
152
+ root: The root path.
153
+ dotted_subpath: Subpath under `root` with dots as separators.
154
+
155
+ Returns:
156
+ The resolved path.
157
+
158
+ Raises:
159
+ I18nValueError: If the given dotted path is invalid.
160
+ """
161
+ *dirs, name = dotted_subpath.split(".")
162
+ if not name:
163
+ raise I18nValueError("Invalid path.")
164
+
165
+ return root / Path(*dirs) / f"{name}.json"
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "htmy"
3
- version = "0.2.0"
3
+ version = "0.3.0"
4
4
  description = "Async, pure-Python rendering engine."
5
5
  authors = ["Peter Volf <do.volfp@gmail.com>"]
6
6
  license = "MIT"
@@ -9,6 +9,7 @@ readme = "README.md"
9
9
  [tool.poetry.dependencies]
10
10
  python = "^3.11"
11
11
  anyio = "^4.6.2.post1"
12
+ async-lru = "^2.0.4"
12
13
  markdown = "^3.7"
13
14
 
14
15
  [tool.poetry.group.dev.dependencies]
@@ -30,6 +31,9 @@ build-backend = "poetry.core.masonry.api"
30
31
  strict = true
31
32
  show_error_codes = true
32
33
 
34
+ [tool.pytest.ini_options]
35
+ addopts = "--random-order"
36
+
33
37
  [tool.ruff]
34
38
  line-length = 108
35
39
  exclude = [
@@ -60,7 +64,7 @@ format = "ruff format ."
60
64
  lint = "ruff check ."
61
65
  lint-fix = "ruff . --fix"
62
66
  mypy = "mypy ."
63
- test = "python -m pytest tests --random-order"
67
+ test = "python -m pytest tests"
64
68
  static-checks.sequence = ["lint", "check-format", "mypy"]
65
69
  static-checks.ignore_fail = "return_non_zero"
66
70
  serve-docs = "mkdocs serve"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes