pyjinhx 0.1.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.
@@ -0,0 +1,10 @@
1
+ [bumpversion]
2
+ current_version = 0.1.0
3
+ commit = True
4
+ tag = True
5
+ message = Bump version: {current_version} → {new_version}
6
+
7
+ [bumpversion:file:pyproject.toml]
8
+ search = version = "{current_version}"
9
+ replace = version = "{new_version}"
10
+
@@ -0,0 +1,45 @@
1
+ # This workflow will upload a Python Package using Twine when a release is created
2
+ # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
3
+
4
+ # This workflow uses actions that are not certified by GitHub.
5
+ # They are provided by a third-party and are governed by
6
+ # separate terms of service, privacy policy, and support
7
+ # documentation.
8
+
9
+ name: Upload Python Package
10
+
11
+ on:
12
+ release:
13
+ types: [published]
14
+
15
+ permissions:
16
+ contents: read
17
+
18
+ jobs:
19
+ deploy:
20
+
21
+ runs-on: ubuntu-latest
22
+
23
+ steps:
24
+ - uses: actions/checkout@v4
25
+ - name: Set up Python
26
+ uses: actions/setup-python@v3
27
+ with:
28
+ python-version: '3.13'
29
+ - name: Install dependencies
30
+ run: |
31
+ python -m pip install --upgrade pip
32
+ pip install build
33
+ - name: List contents of current directory
34
+ run: ls -la
35
+ - name: Clean previous builds
36
+ run: |
37
+ rm -rf build/ dist/
38
+ - name: Build package
39
+ run: |
40
+ python -m build
41
+ - name: Publish package
42
+ uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
43
+ with:
44
+ user: __token__
45
+ password: ${{ secrets.PYPI_API_TOKEN }}
@@ -0,0 +1,28 @@
1
+ name: Ruff
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - '**'
7
+
8
+ jobs:
9
+ lint:
10
+ runs-on: ubuntu-latest
11
+
12
+ steps:
13
+ - name: Check out code
14
+ uses: actions/checkout@v2
15
+
16
+ - name: Set up Python
17
+ uses: actions/setup-python@v2
18
+ with:
19
+ python-version: '3.13'
20
+
21
+ - name: Install Ruff
22
+ run: pip install ruff
23
+
24
+ - name: Run Ruff
25
+ run: ruff format .
26
+
27
+ - name: Run Ruff Check
28
+ run: ruff check .
@@ -0,0 +1,27 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - '**'
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+
12
+ steps:
13
+ - name: Check out code
14
+ uses: actions/checkout@v4
15
+
16
+ - name: Set up Python
17
+ uses: actions/setup-python@v4
18
+ with:
19
+ python-version: '3.13'
20
+
21
+ - name: Install dependencies
22
+ run: |
23
+ pip install -e .
24
+ pip install pytest pytest-asyncio
25
+
26
+ - name: Run tests
27
+ run: pytest tests/
@@ -0,0 +1,10 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
@@ -0,0 +1 @@
1
+ 3.13
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Paulo Mattos
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
pyjinhx-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,151 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyjinhx
3
+ Version: 0.1.0
4
+ Summary: UI components for Python using Pydantic and Jinja2 templates
5
+ Project-URL: Homepage, https://github.com/paulomtts/pyjinhx
6
+ Author-email: Paulo Mattos <paulomtts@outlook.com>
7
+ License: MIT
8
+ License-File: LICENSE.txt
9
+ Keywords: ,components,jinja2,pydantic,templates,ui
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Requires-Python: >=3.13
16
+ Requires-Dist: jinja2>=3.1.6
17
+ Requires-Dist: markupsafe>=3.0.3
18
+ Requires-Dist: pydantic>=2.12.5
19
+ Requires-Dist: pytest>=9.0.1
20
+ Description-Content-Type: text/markdown
21
+
22
+ # PyJinHx
23
+
24
+ Declare reusable, type-safe UI components for template-based web apps in Python. PyJinHx combines Pydantic models with Jinja2 templates to give you automatic template discovery, nested composition, and JavaScript integration—all without manual wiring.
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ pip install pyjinhx
30
+ ```
31
+
32
+ ## Core Capabilities
33
+
34
+ **Automatic Template Discovery**
35
+ - Define a component class and place an HTML template in the same directory with a matching name
36
+ - PyJinHx automatically finds `components/ui/button.html` for a `Button` class in `components/ui/button.py`
37
+ - No manual template path configuration needed
38
+
39
+ **Global Component Registry**
40
+ - Every component automatically registers itself by its `id` when instantiated
41
+ - All registered components are available in any template context by using its id: `{{ component_id }}`
42
+ - Manage the registry state as you wish, have it be request-scoped - or not!
43
+
44
+ **Nested Components**
45
+ - Pass components as fields to other components
46
+ - Nested components are wrapped in an `Object` that provides:
47
+ - `.html` - the rendered HTML string for simple inclusion
48
+ - `.properties` - access to the component instance and its properties
49
+ - Works with single components, lists, and dictionaries
50
+
51
+ **JavaScript Integration**
52
+ - Place a `.js` file next to your component template (e.g., `button.js` next to `button.html`)
53
+ - JavaScript is automatically collected during rendering and bundled into a single `<script>` tag at the root level
54
+ - Specify a custom JS filename with the `js` field
55
+
56
+ **Extra HTML Templates**
57
+ - Include additional HTML files via the `html` field (list of file paths)
58
+ - Each extra template is rendered and added to the context by its filename
59
+ - Access rendered content via `{{ filename.html }}` in your main template
60
+
61
+ ## Technical Details
62
+
63
+ - **Type Safety**: Pydantic models provide validation and IDE support
64
+ - **Template Engine**: Jinja2 with FileSystemLoader (customizable)
65
+ - **Rendering**: Components render via `render()` or automatically via `__html__()`
66
+ - **Context Management**: Thread-safe context variables for registry and script collection
67
+ - **Required Fields**: `id` (unique identifier)
68
+ - **Optional Fields**: `js` (custom JS filename), `html` (list of extra HTML files)
69
+
70
+ ## Complete Example
71
+
72
+ ```python
73
+ # components/ui/button.py
74
+ from pyjinhx import BaseComponent
75
+
76
+ class Button(BaseComponent):
77
+ id: str
78
+ text: str
79
+ variant: str = "primary"
80
+ ```
81
+
82
+ ```html
83
+ <!-- components/ui/button.html -->
84
+ <button id="{{ id }}" class="btn btn-{{ variant }}">{{ text }}</button>
85
+ ```
86
+
87
+ ```javascript
88
+ // components/ui/button.js
89
+ console.log('Button {{ id }} initialized');
90
+ ```
91
+
92
+ ```python
93
+ # components/ui/card.py
94
+ from pyjinhx import BaseComponent
95
+ from components.ui.button import Button
96
+
97
+ class Card(BaseComponent):
98
+ id: str
99
+ title: str
100
+ content: Button
101
+ ```
102
+
103
+ ```html
104
+ <!-- components/ui/card.html -->
105
+ <div id="{{ id }}" class="card">
106
+ <h2>{{ title }}</h2>
107
+ <div class="card-body">
108
+ {{ content.html }}
109
+ </div>
110
+ <div class="card-footer">
111
+ {{ footer.html }}
112
+ </div>
113
+ </div>
114
+ ```
115
+
116
+ ```html
117
+ <!-- components/ui/footer.html -->
118
+ <p class="footer-text">© 2024 My App</p>
119
+ ```
120
+
121
+ ```python
122
+ # Usage
123
+ from components.ui.card import Card
124
+ from components.ui.button import Button
125
+
126
+ action_btn = Button(id="action-1", text="Submit", variant="success")
127
+
128
+ card = Card(
129
+ id="form-card",
130
+ title="User Form",
131
+ content=action_btn,
132
+ html=["components/ui/footer.html"]
133
+ )
134
+
135
+ # Render the component
136
+ html = card.render()
137
+ # The card template can access:
138
+ # - Nested components via .html (e.g., {{ content.html }})
139
+ # - Component properties via .properties (e.g., {{ content.properties.text }})
140
+ # - Extra HTML templates via .html (e.g., {{ footer.html }})
141
+ # - Any registered component by ID (e.g., {{ action-1 }})
142
+ # - All JavaScript files bundled at the end
143
+ ```
144
+
145
+ ```html
146
+ <!-- Render any component by ID in any template -->
147
+ <!-- page.html -->
148
+ <div>{{ form-card }}</div>
149
+ ```
150
+
151
+ This example demonstrates nested components, extra HTML templates, the global registry, Object wrapping with `.html` and `.properties`, automatic template discovery, JavaScript bundling, and rendering components by ID.
@@ -0,0 +1,130 @@
1
+ # PyJinHx
2
+
3
+ Declare reusable, type-safe UI components for template-based web apps in Python. PyJinHx combines Pydantic models with Jinja2 templates to give you automatic template discovery, nested composition, and JavaScript integration—all without manual wiring.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install pyjinhx
9
+ ```
10
+
11
+ ## Core Capabilities
12
+
13
+ **Automatic Template Discovery**
14
+ - Define a component class and place an HTML template in the same directory with a matching name
15
+ - PyJinHx automatically finds `components/ui/button.html` for a `Button` class in `components/ui/button.py`
16
+ - No manual template path configuration needed
17
+
18
+ **Global Component Registry**
19
+ - Every component automatically registers itself by its `id` when instantiated
20
+ - All registered components are available in any template context by using its id: `{{ component_id }}`
21
+ - Manage the registry state as you wish, have it be request-scoped - or not!
22
+
23
+ **Nested Components**
24
+ - Pass components as fields to other components
25
+ - Nested components are wrapped in an `Object` that provides:
26
+ - `.html` - the rendered HTML string for simple inclusion
27
+ - `.properties` - access to the component instance and its properties
28
+ - Works with single components, lists, and dictionaries
29
+
30
+ **JavaScript Integration**
31
+ - Place a `.js` file next to your component template (e.g., `button.js` next to `button.html`)
32
+ - JavaScript is automatically collected during rendering and bundled into a single `<script>` tag at the root level
33
+ - Specify a custom JS filename with the `js` field
34
+
35
+ **Extra HTML Templates**
36
+ - Include additional HTML files via the `html` field (list of file paths)
37
+ - Each extra template is rendered and added to the context by its filename
38
+ - Access rendered content via `{{ filename.html }}` in your main template
39
+
40
+ ## Technical Details
41
+
42
+ - **Type Safety**: Pydantic models provide validation and IDE support
43
+ - **Template Engine**: Jinja2 with FileSystemLoader (customizable)
44
+ - **Rendering**: Components render via `render()` or automatically via `__html__()`
45
+ - **Context Management**: Thread-safe context variables for registry and script collection
46
+ - **Required Fields**: `id` (unique identifier)
47
+ - **Optional Fields**: `js` (custom JS filename), `html` (list of extra HTML files)
48
+
49
+ ## Complete Example
50
+
51
+ ```python
52
+ # components/ui/button.py
53
+ from pyjinhx import BaseComponent
54
+
55
+ class Button(BaseComponent):
56
+ id: str
57
+ text: str
58
+ variant: str = "primary"
59
+ ```
60
+
61
+ ```html
62
+ <!-- components/ui/button.html -->
63
+ <button id="{{ id }}" class="btn btn-{{ variant }}">{{ text }}</button>
64
+ ```
65
+
66
+ ```javascript
67
+ // components/ui/button.js
68
+ console.log('Button {{ id }} initialized');
69
+ ```
70
+
71
+ ```python
72
+ # components/ui/card.py
73
+ from pyjinhx import BaseComponent
74
+ from components.ui.button import Button
75
+
76
+ class Card(BaseComponent):
77
+ id: str
78
+ title: str
79
+ content: Button
80
+ ```
81
+
82
+ ```html
83
+ <!-- components/ui/card.html -->
84
+ <div id="{{ id }}" class="card">
85
+ <h2>{{ title }}</h2>
86
+ <div class="card-body">
87
+ {{ content.html }}
88
+ </div>
89
+ <div class="card-footer">
90
+ {{ footer.html }}
91
+ </div>
92
+ </div>
93
+ ```
94
+
95
+ ```html
96
+ <!-- components/ui/footer.html -->
97
+ <p class="footer-text">© 2024 My App</p>
98
+ ```
99
+
100
+ ```python
101
+ # Usage
102
+ from components.ui.card import Card
103
+ from components.ui.button import Button
104
+
105
+ action_btn = Button(id="action-1", text="Submit", variant="success")
106
+
107
+ card = Card(
108
+ id="form-card",
109
+ title="User Form",
110
+ content=action_btn,
111
+ html=["components/ui/footer.html"]
112
+ )
113
+
114
+ # Render the component
115
+ html = card.render()
116
+ # The card template can access:
117
+ # - Nested components via .html (e.g., {{ content.html }})
118
+ # - Component properties via .properties (e.g., {{ content.properties.text }})
119
+ # - Extra HTML templates via .html (e.g., {{ footer.html }})
120
+ # - Any registered component by ID (e.g., {{ action-1 }})
121
+ # - All JavaScript files bundled at the end
122
+ ```
123
+
124
+ ```html
125
+ <!-- Render any component by ID in any template -->
126
+ <!-- page.html -->
127
+ <div>{{ form-card }}</div>
128
+ ```
129
+
130
+ This example demonstrates nested components, extra HTML templates, the global registry, Object wrapping with `.html` and `.properties`, automatic template discovery, JavaScript bundling, and rendering components by ID.
@@ -0,0 +1,3 @@
1
+ from .base import BaseComponent, Registry
2
+
3
+ __all__ = ["BaseComponent", "Registry"]
@@ -0,0 +1,252 @@
1
+ import inspect
2
+ import logging
3
+ import os
4
+ import re
5
+ from contextvars import ContextVar
6
+ from typing import Any, ClassVar, Optional
7
+
8
+ from jinja2 import Environment, FileSystemLoader, Template
9
+ from markupsafe import Markup
10
+ from pydantic import BaseModel, Field, field_validator
11
+
12
+ logger = logging.getLogger("pyjinhx")
13
+ logger.setLevel(logging.WARNING)
14
+
15
+ _registry_context: ContextVar[dict[str, "BaseComponent"]] = ContextVar(
16
+ "component_registry", default={}
17
+ )
18
+
19
+ _scripts_context: ContextVar[list[str]] = ContextVar(
20
+ "scripts_collection", default=[]
21
+ )
22
+
23
+
24
+ class Registry:
25
+ """
26
+ Registry for all components.
27
+ """
28
+
29
+ @classmethod
30
+ def register(cls, component: "BaseComponent") -> None:
31
+ registry = _registry_context.get()
32
+ if component.id in registry:
33
+ logger.warning(
34
+ f"While registering{component.__class__.__name__}(id={component.id}) found an existing component with the same id. Overwriting..."
35
+ )
36
+ registry[component.id] = component
37
+
38
+ @classmethod
39
+ def clear(cls) -> None:
40
+ _registry_context.set({})
41
+
42
+ @classmethod
43
+ def get(cls) -> dict[str, "BaseComponent"]:
44
+ return _registry_context.get()
45
+
46
+ class Object(BaseModel):
47
+ """
48
+ A wrapper for nested components. Enables access to the component's properties and rendered HTML.
49
+ """
50
+ html: str
51
+ properties: Optional["BaseComponent"]
52
+
53
+
54
+
55
+ class BaseComponent(BaseModel):
56
+ "Provides functionality for declaring UI components in python."
57
+
58
+ _engine: ClassVar[Optional[Environment]] = None
59
+
60
+ @classmethod
61
+ def set_engine(cls, environment: Environment):
62
+ """
63
+ Sets the Jinja2 environment for all components that inherit from this base class.
64
+ This should be called once at application startup if the root directory auto-detection fails.
65
+ """
66
+ cls._engine = environment
67
+
68
+ @classmethod
69
+ def _detect_root_directory(cls) -> str:
70
+ """
71
+ Attempts to detect a reasonable root directory for the template loader.
72
+ Looks for common project markers or uses the current working directory.
73
+ """
74
+ current_dir = os.getcwd()
75
+
76
+ project_markers = ["pyproject.toml", "main.py", "README.md", ".git"]
77
+
78
+ search_dir = current_dir
79
+ while search_dir != os.path.dirname(search_dir):
80
+ for marker in project_markers:
81
+ if os.path.exists(os.path.join(search_dir, marker)):
82
+ return search_dir
83
+ search_dir = os.path.dirname(search_dir)
84
+
85
+ return current_dir
86
+
87
+ id: str = Field(..., description="The unique ID for this component.")
88
+ js: Optional[str] = Field(
89
+ default=None, description="The JavaScript file for this component."
90
+ )
91
+ html: list[str] = Field(
92
+ default_factory=list, description="Extra HTML files to add to the component."
93
+ )
94
+
95
+ @classmethod
96
+ def _ensure_engine(cls) -> Environment:
97
+ """
98
+ Ensures the Jinja2 environment is initialized.
99
+ Creates it automatically if not already set.
100
+ """
101
+ if cls._engine is None:
102
+ root_dir = cls._detect_root_directory()
103
+ cls._engine = Environment(loader=FileSystemLoader(root_dir))
104
+ return cls._engine
105
+
106
+ @field_validator("id", mode="before")
107
+ def validate_id(cls, v):
108
+ if not v:
109
+ raise ValueError("ID is required")
110
+ return str(v)
111
+
112
+ def __init__(self, **kwargs):
113
+ super().__init__(**kwargs)
114
+ Registry.register(self)
115
+
116
+ def __html__(self) -> Markup:
117
+ """
118
+ Automatically renders the component when accessed.
119
+ This allows for cleaner template syntax: {{ MyComponent }} instead of {{ MyComponent.render() }}
120
+ """
121
+ return self.render()
122
+
123
+ def _get_snake_case_name(self, name: str | None = None) -> str:
124
+ if name is None:
125
+ name = self.__class__.__name__
126
+ return re.sub(r"(?<!^)(?=[A-Z])", "_", name).lower()
127
+
128
+ def _get_raw_path(self) -> str:
129
+ return os.path.dirname(inspect.getfile(self.__class__)).replace("\\", "/")
130
+
131
+ def _get_relative_path(self, name: str | None = None) -> str:
132
+ raw_path = self._get_raw_path()
133
+ snake_case_name = self._get_snake_case_name(name)
134
+
135
+ engine = BaseComponent._ensure_engine()
136
+ loader = engine.loader
137
+ if not isinstance(loader, FileSystemLoader):
138
+ raise ValueError("Jinja2 loader must be a FileSystemLoader")
139
+
140
+ search_path = (
141
+ loader.searchpath[0]
142
+ if isinstance(loader.searchpath, list)
143
+ else loader.searchpath
144
+ )
145
+ relative_dir = os.path.relpath(raw_path, search_path).replace("\\", "/")
146
+
147
+ return f"{relative_dir}/{snake_case_name}.html"
148
+
149
+ def _get_js_file_name(self) -> str | None:
150
+ raw_path = self._get_raw_path()
151
+ snake_case_name = self.js if self.js else self._get_snake_case_name()
152
+ js_file_name = snake_case_name.replace("_", "-") + ("" if self.js else ".js")
153
+ if not os.path.exists(f"{raw_path}/{js_file_name}"):
154
+ return None
155
+ return js_file_name
156
+
157
+ def _load_template(self, source: str | None = None) -> Template:
158
+ engine = BaseComponent._ensure_engine()
159
+ if source is None:
160
+ relative_path = self._get_relative_path()
161
+ return engine.get_template(relative_path)
162
+ else:
163
+ return engine.from_string(source)
164
+
165
+ def _update_context(
166
+ self,
167
+ context: dict[str, Any],
168
+ field_name: str,
169
+ field_value: Any,
170
+ ) -> dict[str, Any]:
171
+ """
172
+ Updates the context with rendered components by their ID.
173
+ """
174
+ if isinstance(field_value, BaseComponent):
175
+ context[field_name] = Object(html=field_value.render(base_context=context), properties=field_value)
176
+ elif isinstance(field_value, list):
177
+ for item in field_value:
178
+ if isinstance(item, BaseComponent):
179
+ context[field_name] = Object(html=item.render(base_context=context), properties=item)
180
+ elif isinstance(field_value, dict) and all(
181
+ isinstance(value, BaseComponent) for value in field_value.values()
182
+ ):
183
+ for item in field_value.values():
184
+ if isinstance(item, BaseComponent):
185
+ context[field_name] = Object(html=item.render(base_context=context), properties=item)
186
+ return context
187
+
188
+ def _get_javascript_content(self) -> str | None:
189
+ js_file_name = self._get_js_file_name()
190
+ if js_file_name:
191
+ raw_path = self._get_raw_path()
192
+ js_path = f"{raw_path}/{js_file_name}"
193
+ if os.path.exists(js_path):
194
+ with open(js_path, "r") as f:
195
+ return f.read()
196
+ return None
197
+
198
+ def render(
199
+ self, source: str | None = None, base_context: dict[str, Any] | None = None
200
+ ) -> Markup:
201
+ """
202
+ Renders the component's template with the given context - including the global components.
203
+
204
+ Returns:
205
+ Markup: The rendered component.
206
+ """
207
+ is_root = base_context is None
208
+ if is_root:
209
+ _scripts_context.set([])
210
+
211
+ # 1. Load context & template
212
+ if base_context is None:
213
+ context = self.model_dump()
214
+ else:
215
+ context = {**base_context, **self.model_dump()}
216
+ template = self._load_template(source)
217
+
218
+ # 2. Render nested components
219
+ if is_root:
220
+ for field_name in type(self).model_fields.keys():
221
+ field_value = getattr(self, field_name)
222
+ context = self._update_context(context, field_name, field_value)
223
+
224
+ # 3. Update context with all components & extra HTML templates
225
+ context.update(Registry.get())
226
+ if is_root:
227
+ for html_file in self.html:
228
+ with open(html_file, "r") as file:
229
+ html_template = file.read()
230
+ extra_markup = self.render(html_template, context)
231
+ html_key = html_file.split("/")[-1].split(".")[0]
232
+ context[html_key] = Object(html=extra_markup, properties=None)
233
+
234
+ # 4. Render template
235
+ rendered_template = template.render(context)
236
+
237
+ # 5. Collect JavaScript from this component (only for component's own template, not extra HTML)
238
+ if source is None:
239
+ js_content = self._get_javascript_content()
240
+ if js_content:
241
+ scripts = _scripts_context.get()
242
+ scripts.append(js_content)
243
+ _scripts_context.set(scripts)
244
+
245
+ # 6. Append all collected scripts at root level
246
+ if is_root:
247
+ scripts = _scripts_context.get()
248
+ if scripts:
249
+ combined_script = "\n".join(scripts)
250
+ rendered_template += f"\n<script>{combined_script}</script>"
251
+
252
+ return Markup(rendered_template).unescape()
@@ -0,0 +1,31 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "pyjinhx"
7
+ version = "0.1.0"
8
+ description = "UI components for Python using Pydantic and Jinja2 templates"
9
+ readme = "README.md"
10
+ requires-python = ">=3.13"
11
+ license = {text = "MIT"}
12
+ authors = [
13
+ {name = "Paulo Mattos", email = "paulomtts@outlook.com"}
14
+ ]
15
+ keywords = ["jinja2", "pydantic", "components", "templates", "", "ui"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.13",
22
+ ]
23
+ dependencies = [
24
+ "jinja2>=3.1.6",
25
+ "markupsafe>=3.0.3",
26
+ "pydantic>=2.12.5",
27
+ "pytest>=9.0.1",
28
+ ]
29
+
30
+ [project.urls]
31
+ Homepage = "https://github.com/paulomtts/pyjinhx"
@@ -0,0 +1,10 @@
1
+ from tests.ui.button import Button
2
+
3
+
4
+ def test_button_render():
5
+ button = Button(id="test-button", text="Click Me")
6
+ rendered = button.render()
7
+ expected = '<button id="test-button">Click Me</button>\n<script>console.log(\'Button loaded\');</script>'
8
+
9
+ assert str(rendered) == expected
10
+
@@ -0,0 +1,20 @@
1
+ from tests.ui.card import Card
2
+ from tests.ui.button import Button
3
+
4
+
5
+ def test_nested_component():
6
+ button = Button(id="action-btn", text="Click Me")
7
+ card = Card(id="card-1", title="My Card", content=button, html=["tests/ui/span.html"])
8
+
9
+ rendered = card.render()
10
+
11
+ assert rendered == """<div id="card-1" class="card">
12
+ <h2>My Card</h2>
13
+ <div class="card-content">
14
+ <button id="action-btn">Click Me</button>
15
+ <span>Extra HTML Content</span>
16
+ </div>
17
+ </div>
18
+ <script>console.log('Button loaded');
19
+ console.log('Card loaded');</script>"""
20
+
@@ -0,0 +1 @@
1
+ <button id="{{ id }}">{{ text }}</button>
@@ -0,0 +1 @@
1
+ console.log('Button loaded');
@@ -0,0 +1,7 @@
1
+ from pyjinhx import BaseComponent
2
+
3
+
4
+ class Button(BaseComponent):
5
+ id: str
6
+ text: str
7
+
@@ -0,0 +1,7 @@
1
+ <div id="{{ id }}" class="card">
2
+ <h2>{{ title }}</h2>
3
+ <div class="card-content">
4
+ {{ content.html }}
5
+ {{ span.html }}
6
+ </div>
7
+ </div>
@@ -0,0 +1 @@
1
+ console.log('Card loaded');
@@ -0,0 +1,9 @@
1
+ from pyjinhx import BaseComponent
2
+ from tests.ui.button import Button
3
+
4
+
5
+ class Card(BaseComponent):
6
+ id: str
7
+ title: str
8
+ content: Button
9
+
@@ -0,0 +1 @@
1
+ <span>Extra HTML Content</span>
pyjinhx-0.1.0/uv.lock ADDED
@@ -0,0 +1,245 @@
1
+ version = 1
2
+ revision = 3
3
+ requires-python = ">=3.13"
4
+
5
+ [[package]]
6
+ name = "annotated-types"
7
+ version = "0.7.0"
8
+ source = { registry = "https://pypi.org/simple" }
9
+ sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
10
+ wheels = [
11
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
12
+ ]
13
+
14
+ [[package]]
15
+ name = "colorama"
16
+ version = "0.4.6"
17
+ source = { registry = "https://pypi.org/simple" }
18
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
19
+ wheels = [
20
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
21
+ ]
22
+
23
+ [[package]]
24
+ name = "iniconfig"
25
+ version = "2.3.0"
26
+ source = { registry = "https://pypi.org/simple" }
27
+ sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
28
+ wheels = [
29
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
30
+ ]
31
+
32
+ [[package]]
33
+ name = "jinja2"
34
+ version = "3.1.6"
35
+ source = { registry = "https://pypi.org/simple" }
36
+ dependencies = [
37
+ { name = "markupsafe" },
38
+ ]
39
+ sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
40
+ wheels = [
41
+ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
42
+ ]
43
+
44
+ [[package]]
45
+ name = "markupsafe"
46
+ version = "3.0.3"
47
+ source = { registry = "https://pypi.org/simple" }
48
+ sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
49
+ wheels = [
50
+ { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
51
+ { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
52
+ { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
53
+ { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
54
+ { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
55
+ { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
56
+ { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
57
+ { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
58
+ { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
59
+ { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
60
+ { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
61
+ { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
62
+ { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
63
+ { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
64
+ { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
65
+ { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
66
+ { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
67
+ { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
68
+ { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
69
+ { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
70
+ { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
71
+ { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
72
+ { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
73
+ { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
74
+ { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
75
+ { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
76
+ { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
77
+ { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
78
+ { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
79
+ { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
80
+ { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
81
+ { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
82
+ { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
83
+ { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
84
+ { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
85
+ { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
86
+ { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
87
+ { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
88
+ { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
89
+ { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
90
+ { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
91
+ { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
92
+ { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
93
+ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
94
+ ]
95
+
96
+ [[package]]
97
+ name = "packaging"
98
+ version = "25.0"
99
+ source = { registry = "https://pypi.org/simple" }
100
+ sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
101
+ wheels = [
102
+ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
103
+ ]
104
+
105
+ [[package]]
106
+ name = "pluggy"
107
+ version = "1.6.0"
108
+ source = { registry = "https://pypi.org/simple" }
109
+ sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
110
+ wheels = [
111
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
112
+ ]
113
+
114
+ [[package]]
115
+ name = "pydantic"
116
+ version = "2.12.5"
117
+ source = { registry = "https://pypi.org/simple" }
118
+ dependencies = [
119
+ { name = "annotated-types" },
120
+ { name = "pydantic-core" },
121
+ { name = "typing-extensions" },
122
+ { name = "typing-inspection" },
123
+ ]
124
+ sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" }
125
+ wheels = [
126
+ { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
127
+ ]
128
+
129
+ [[package]]
130
+ name = "pydantic-core"
131
+ version = "2.41.5"
132
+ source = { registry = "https://pypi.org/simple" }
133
+ dependencies = [
134
+ { name = "typing-extensions" },
135
+ ]
136
+ sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
137
+ wheels = [
138
+ { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" },
139
+ { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" },
140
+ { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" },
141
+ { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" },
142
+ { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" },
143
+ { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" },
144
+ { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" },
145
+ { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" },
146
+ { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" },
147
+ { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" },
148
+ { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" },
149
+ { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" },
150
+ { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" },
151
+ { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" },
152
+ { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" },
153
+ { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" },
154
+ { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" },
155
+ { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" },
156
+ { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" },
157
+ { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" },
158
+ { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" },
159
+ { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" },
160
+ { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" },
161
+ { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" },
162
+ { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" },
163
+ { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" },
164
+ { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" },
165
+ { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" },
166
+ { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" },
167
+ { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" },
168
+ { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" },
169
+ { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" },
170
+ { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" },
171
+ { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" },
172
+ { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" },
173
+ { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" },
174
+ { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" },
175
+ { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" },
176
+ { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" },
177
+ { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
178
+ { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
179
+ { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
180
+ ]
181
+
182
+ [[package]]
183
+ name = "pygments"
184
+ version = "2.19.2"
185
+ source = { registry = "https://pypi.org/simple" }
186
+ sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
187
+ wheels = [
188
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
189
+ ]
190
+
191
+ [[package]]
192
+ name = "pyjinhx"
193
+ version = "0.1.0"
194
+ source = { editable = "." }
195
+ dependencies = [
196
+ { name = "jinja2" },
197
+ { name = "markupsafe" },
198
+ { name = "pydantic" },
199
+ { name = "pytest" },
200
+ ]
201
+
202
+ [package.metadata]
203
+ requires-dist = [
204
+ { name = "jinja2", specifier = ">=3.1.6" },
205
+ { name = "markupsafe", specifier = ">=3.0.3" },
206
+ { name = "pydantic", specifier = ">=2.12.5" },
207
+ { name = "pytest", specifier = ">=9.0.1" },
208
+ ]
209
+
210
+ [[package]]
211
+ name = "pytest"
212
+ version = "9.0.1"
213
+ source = { registry = "https://pypi.org/simple" }
214
+ dependencies = [
215
+ { name = "colorama", marker = "sys_platform == 'win32'" },
216
+ { name = "iniconfig" },
217
+ { name = "packaging" },
218
+ { name = "pluggy" },
219
+ { name = "pygments" },
220
+ ]
221
+ sdist = { url = "https://files.pythonhosted.org/packages/07/56/f013048ac4bc4c1d9be45afd4ab209ea62822fb1598f40687e6bf45dcea4/pytest-9.0.1.tar.gz", hash = "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8", size = 1564125, upload-time = "2025-11-12T13:05:09.333Z" }
222
+ wheels = [
223
+ { url = "https://files.pythonhosted.org/packages/0b/8b/6300fb80f858cda1c51ffa17075df5d846757081d11ab4aa35cef9e6258b/pytest-9.0.1-py3-none-any.whl", hash = "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad", size = 373668, upload-time = "2025-11-12T13:05:07.379Z" },
224
+ ]
225
+
226
+ [[package]]
227
+ name = "typing-extensions"
228
+ version = "4.15.0"
229
+ source = { registry = "https://pypi.org/simple" }
230
+ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
231
+ wheels = [
232
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
233
+ ]
234
+
235
+ [[package]]
236
+ name = "typing-inspection"
237
+ version = "0.4.2"
238
+ source = { registry = "https://pypi.org/simple" }
239
+ dependencies = [
240
+ { name = "typing-extensions" },
241
+ ]
242
+ sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
243
+ wheels = [
244
+ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
245
+ ]