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.
- pyjinhx-0.1.0/.bumpversion.cfg +10 -0
- pyjinhx-0.1.0/.github/workflows/publish.yml +45 -0
- pyjinhx-0.1.0/.github/workflows/ruff.yml +28 -0
- pyjinhx-0.1.0/.github/workflows/tests.yml +27 -0
- pyjinhx-0.1.0/.gitignore +10 -0
- pyjinhx-0.1.0/.python-version +1 -0
- pyjinhx-0.1.0/LICENSE.txt +21 -0
- pyjinhx-0.1.0/PKG-INFO +151 -0
- pyjinhx-0.1.0/README.md +130 -0
- pyjinhx-0.1.0/pyjinhx/__init__.py +3 -0
- pyjinhx-0.1.0/pyjinhx/base.py +252 -0
- pyjinhx-0.1.0/pyproject.toml +31 -0
- pyjinhx-0.1.0/tests/button_test.py +10 -0
- pyjinhx-0.1.0/tests/nested_component_test.py +20 -0
- pyjinhx-0.1.0/tests/ui/button.html +1 -0
- pyjinhx-0.1.0/tests/ui/button.js +1 -0
- pyjinhx-0.1.0/tests/ui/button.py +7 -0
- pyjinhx-0.1.0/tests/ui/card.html +7 -0
- pyjinhx-0.1.0/tests/ui/card.js +1 -0
- pyjinhx-0.1.0/tests/ui/card.py +9 -0
- pyjinhx-0.1.0/tests/ui/span.html +1 -0
- pyjinhx-0.1.0/uv.lock +245 -0
|
@@ -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/
|
pyjinhx-0.1.0/.gitignore
ADDED
|
@@ -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.
|
pyjinhx-0.1.0/README.md
ADDED
|
@@ -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,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 @@
|
|
|
1
|
+
console.log('Card loaded');
|
|
@@ -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
|
+
]
|