pyh5p 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.
Files changed (45) hide show
  1. pyh5p-0.1.0/.github/workflows/ci.yml +23 -0
  2. pyh5p-0.1.0/.gitignore +33 -0
  3. pyh5p-0.1.0/CHANGELOG.md +11 -0
  4. pyh5p-0.1.0/LICENSE +22 -0
  5. pyh5p-0.1.0/PKG-INFO +245 -0
  6. pyh5p-0.1.0/README.md +186 -0
  7. pyh5p-0.1.0/answers.json +13 -0
  8. pyh5p-0.1.0/examples/round_trip.py +7 -0
  9. pyh5p-0.1.0/fixtures/README.md +17 -0
  10. pyh5p-0.1.0/fixtures/examples/synthetic.json +10 -0
  11. pyh5p-0.1.0/fixtures/libraries/H5P.NestedFixture-1.0/library.js +1 -0
  12. pyh5p-0.1.0/fixtures/libraries/H5P.NestedFixture-1.0/library.json +9 -0
  13. pyh5p-0.1.0/fixtures/libraries/H5P.NestedFixture-1.0/semantics.json +3 -0
  14. pyh5p-0.1.0/fixtures/libraries/H5P.SyntheticAllTypes-1.0/css/synthetic.css +1 -0
  15. pyh5p-0.1.0/fixtures/libraries/H5P.SyntheticAllTypes-1.0/js/synthetic.js +2 -0
  16. pyh5p-0.1.0/fixtures/libraries/H5P.SyntheticAllTypes-1.0/library.json +17 -0
  17. pyh5p-0.1.0/fixtures/libraries/H5P.SyntheticAllTypes-1.0/presave.js +2 -0
  18. pyh5p-0.1.0/fixtures/libraries/H5P.SyntheticAllTypes-1.0/semantics.json +15 -0
  19. pyh5p-0.1.0/fixtures/libraries/H5P.SyntheticAllTypes-1.0/upgrades.js +2 -0
  20. pyh5p-0.1.0/fixtures/libraries/H5P.SyntheticDependency-1.0/library.js +1 -0
  21. pyh5p-0.1.0/fixtures/libraries/H5P.SyntheticDependency-1.0/library.json +9 -0
  22. pyh5p-0.1.0/fixtures/libraries/H5P.SyntheticDynamic-1.0/library.js +1 -0
  23. pyh5p-0.1.0/fixtures/libraries/H5P.SyntheticDynamic-1.0/library.json +8 -0
  24. pyh5p-0.1.0/fixtures/libraries/H5PEditor.SyntheticWidget-1.0/library.js +1 -0
  25. pyh5p-0.1.0/fixtures/libraries/H5PEditor.SyntheticWidget-1.0/library.json +8 -0
  26. pyh5p-0.1.0/pyproject.toml +69 -0
  27. pyh5p-0.1.0/scripts/differential_check.py +51 -0
  28. pyh5p-0.1.0/scripts/discover_types.py +53 -0
  29. pyh5p-0.1.0/src/pyh5p/__init__.py +44 -0
  30. pyh5p-0.1.0/src/pyh5p/_util.py +90 -0
  31. pyh5p-0.1.0/src/pyh5p/cli.py +169 -0
  32. pyh5p-0.1.0/src/pyh5p/content.py +567 -0
  33. pyh5p-0.1.0/src/pyh5p/errors.py +53 -0
  34. pyh5p-0.1.0/src/pyh5p/hooks.py +55 -0
  35. pyh5p-0.1.0/src/pyh5p/library.py +850 -0
  36. pyh5p-0.1.0/src/pyh5p/package.py +267 -0
  37. pyh5p-0.1.0/src/pyh5p/py.typed +1 -0
  38. pyh5p-0.1.0/src/pyh5p/security.py +183 -0
  39. pyh5p-0.1.0/src/pyh5p/semantics.py +643 -0
  40. pyh5p-0.1.0/tests/conftest.py +15 -0
  41. pyh5p-0.1.0/tests/test_hooks.py +9 -0
  42. pyh5p-0.1.0/tests/test_library.py +173 -0
  43. pyh5p-0.1.0/tests/test_packages.py +126 -0
  44. pyh5p-0.1.0/tests/test_properties.py +22 -0
  45. pyh5p-0.1.0/tests/test_semantics.py +80 -0
@@ -0,0 +1,23 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ jobs:
8
+ test:
9
+ strategy:
10
+ fail-fast: false
11
+ matrix:
12
+ os: [ubuntu-latest, windows-latest, macos-latest]
13
+ python: ["3.10", "3.11", "3.12", "3.13", "3.14"]
14
+ runs-on: ${{ matrix.os }}
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: actions/setup-python@v5
18
+ with:
19
+ python-version: ${{ matrix.python }}
20
+ - run: python -m pip install -e ".[dev]"
21
+ - run: python -m pytest --cov=pyh5p --cov-report=term-missing
22
+ - run: ruff check .
23
+ - run: mypy src/pyh5p
pyh5p-0.1.0/.gitignore ADDED
@@ -0,0 +1,33 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .DS_Store
4
+ Thumbs.db
5
+
6
+ .pytest_cache/
7
+ .pytest-tmp/
8
+ .pytest-*/
9
+ .hypothesis/
10
+ .mypy_cache/
11
+ .ruff_cache/
12
+ .coverage
13
+ coverage.xml
14
+ htmlcov/
15
+
16
+ dist/
17
+ build/
18
+ *.egg-info/
19
+ *.whl
20
+
21
+ .venv/
22
+ .release-test/
23
+ .env
24
+ .tox/
25
+ .nox/
26
+ .pyright/
27
+
28
+ # Local H5P outputs and downloaded library caches.
29
+ *.h5p
30
+ *.zip
31
+ .pyh5p-libraries/
32
+ unpacked/
33
+ fixtures/downloaded/
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 - 2026-09-12
4
+
5
+ Initial public release of the generic semantics engine, local library store,
6
+ Hub and registry clients, secure package reader/writer, validator, JSON Schema
7
+ exporter, CLI, fixture tooling, and optional JavaScript hook boundary.
8
+
9
+ Automatic builds now use complete distributable Hub packages, including built
10
+ runtime files. The CLI can target the historical Core API 1.27 package set
11
+ with `--core-api 1.27`.
pyh5p-0.1.0/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nikita Berger
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.
22
+
pyh5p-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,245 @@
1
+ Metadata-Version: 2.5
2
+ Name: pyh5p
3
+ Version: 0.1.0
4
+ Summary: Schema-driven creation, inspection, validation and packaging of H5P content
5
+ Project-URL: Homepage, https://github.com/Nikityyy/pyh5p
6
+ Project-URL: Repository, https://github.com/Nikityyy/pyh5p
7
+ Project-URL: Issues, https://github.com/Nikityyy/pyh5p/issues
8
+ Project-URL: Author, https://nikity.is-a.dev
9
+ Author-email: Nikita Berger <bergernikita1807@gmail.com>
10
+ License: MIT License
11
+
12
+ Copyright (c) 2026 Nikita Berger
13
+
14
+ Permission is hereby granted, free of charge, to any person obtaining a copy
15
+ of this software and associated documentation files (the "Software"), to deal
16
+ in the Software without restriction, including without limitation the rights
17
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
+ copies of the Software, and to permit persons to whom the Software is
19
+ furnished to do so, subject to the following conditions:
20
+
21
+ The above copyright notice and this permission notice shall be included in all
22
+ copies or substantial portions of the Software.
23
+
24
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
+ SOFTWARE.
31
+
32
+ License-File: LICENSE
33
+ Keywords: content,elearning,h5p,html5,lms
34
+ Classifier: Development Status :: 3 - Alpha
35
+ Classifier: Intended Audience :: Developers
36
+ Classifier: License :: OSI Approved :: MIT License
37
+ Classifier: Programming Language :: Python :: 3
38
+ Classifier: Programming Language :: Python :: 3.10
39
+ Classifier: Programming Language :: Python :: 3.11
40
+ Classifier: Programming Language :: Python :: 3.12
41
+ Classifier: Programming Language :: Python :: 3.13
42
+ Classifier: Programming Language :: Python :: 3.14
43
+ Classifier: Topic :: Education
44
+ Requires-Python: >=3.10
45
+ Provides-Extra: dev
46
+ Requires-Dist: build>=1.2; extra == 'dev'
47
+ Requires-Dist: hatchling>=1.25; extra == 'dev'
48
+ Requires-Dist: hypothesis>=6.100; extra == 'dev'
49
+ Requires-Dist: mypy>=1.10; extra == 'dev'
50
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
51
+ Requires-Dist: pytest>=8.0; extra == 'dev'
52
+ Requires-Dist: ruff>=0.6; extra == 'dev'
53
+ Requires-Dist: twine>=5.0; extra == 'dev'
54
+ Provides-Extra: html
55
+ Requires-Dist: nh3>=0.2; extra == 'html'
56
+ Provides-Extra: js
57
+ Requires-Dist: quickjs>=1.19; extra == 'js'
58
+ Description-Content-Type: text/markdown
59
+
60
+ # pyh5p
61
+
62
+ `pyh5p` creates, edits, validates, and packages H5P content in Python. It
63
+ reads each library's `library.json` and `semantics.json`, so the Python code
64
+ does not need a class for every H5P content type.
65
+
66
+ ## Quick start
67
+
68
+ Install the package:
69
+
70
+ ```text
71
+ pip install pyh5p
72
+ ```
73
+
74
+ Put the content data in `answers.json`:
75
+
76
+ ```json
77
+ {
78
+ "question": "What is 2 + 2?",
79
+ "answers": [
80
+ {"text": "4", "correct": true},
81
+ {"text": "5", "correct": false}
82
+ ]
83
+ }
84
+ ```
85
+
86
+ Build the package:
87
+
88
+ ```text
89
+ pyh5p build H5P.MultiChoice answers.json question.h5p
90
+ ```
91
+
92
+ The command fetches a complete package and its dependencies from the official
93
+ H5P Content Type Hub when they are not already cached. Fetching is enabled by
94
+ default. `question.h5p` is ready to import into an H5P host.
95
+
96
+ For Lumi Desktop or Moodle installations that run Core API 1.27, request the
97
+ matching historical package set:
98
+
99
+ ```text
100
+ pyh5p build H5P.MultiChoice answers.json question.h5p --core-api 1.27
101
+ ```
102
+
103
+ ## Python API
104
+
105
+ The same operation is available without a template or content-type-specific
106
+ Python code:
107
+
108
+ ```python
109
+ import pyh5p
110
+
111
+ content = pyh5p.build(
112
+ "H5P.MultiChoice",
113
+ {
114
+ "question": "What is 2 + 2?",
115
+ "answers": [
116
+ {"text": "4", "correct": True},
117
+ {"text": "5", "correct": False},
118
+ ],
119
+ },
120
+ )
121
+ content.save("question.h5p")
122
+ ```
123
+
124
+ The field names and validation rules come from the installed library's
125
+ semantics. The same API works with other runnable libraries, including local,
126
+ historical, and third-party libraries.
127
+
128
+ Media uses the same generic path. For example:
129
+
130
+ ```python
131
+ content = pyh5p.build("H5P.Image", {"decorative": True}, validate=False)
132
+ content.import_media(
133
+ "file", b"image bytes", target="images/example.png", mime="image/png"
134
+ )
135
+ content.save("image.h5p")
136
+ ```
137
+
138
+ `LibraryStore` accepts local library directories and archives. Use it for
139
+ offline builds or for a controlled library set:
140
+
141
+ ```python
142
+ store = pyh5p.LibraryStore("libraries")
143
+ content = pyh5p.build("H5P.MultiChoice", data, store=store, fetch=False)
144
+ ```
145
+
146
+ ## What is generic
147
+
148
+ The semantics engine handles groups, lists, nested libraries, text and HTML,
149
+ numbers, booleans, selects, image/video/audio/file fields, defaults, optional
150
+ values, regular expressions, ranges, allowed tags, widgets, `showWhen`,
151
+ `isSubContent`, unknown attributes, and recursive dependencies.
152
+
153
+ It also supports dependency closure for preloaded, dynamic, editor, and
154
+ nested-content libraries. Packages can be loaded, edited, validated, and
155
+ exported again. Builds use stable JSON ordering and ZIP timestamps where the
156
+ H5P format allows it.
157
+
158
+ ## Command line
159
+
160
+ ```text
161
+ pyh5p build H5P.MultiChoice answers.json question.h5p
162
+ pyh5p inspect question.h5p
163
+ pyh5p validate question.h5p
164
+ pyh5p schema H5P.MultiChoice --libraries .pyh5p-libraries -o schema.json
165
+ pyh5p unpack question.h5p unpacked
166
+ ```
167
+
168
+ Use `--offline` with `build` when network access is not allowed. Use
169
+ `--cache-dir` to choose the library cache. `--content-only` is available when
170
+ the destination explicitly accepts a package without libraries.
171
+
172
+ To preview a package, import it into [Lumi Desktop](https://lumi.education/en/lumi-h5p-offline-desktop-editor/)
173
+ and open its preview. The H5P host runs the library JavaScript; pyh5p creates
174
+ the content and portable package.
175
+
176
+ ## JSON Schema and AI applications
177
+
178
+ Ask pyh5p for the schema, pass it to any provider's structured-output API,
179
+ then validate and build the returned object:
180
+
181
+ ```python
182
+ schema = pyh5p.schema_json("H5P.MultiChoice", store=store)
183
+ returned = provider.generate_json(schema=schema)
184
+ content = pyh5p.build("H5P.MultiChoice", returned, store=store)
185
+ content.require_valid()
186
+ ```
187
+
188
+ The core package has no dependency on an AI provider.
189
+
190
+ ## Compatibility and JavaScript hooks
191
+
192
+ Compatibility means that pyh5p parses the supplied definitions, validates the
193
+ semantic data, resolves dependencies, preserves distributable library files,
194
+ and writes the standard H5P package structure. The host must support the Core
195
+ API version declared by the selected libraries. Current Hub packages may
196
+ require Core API 1.28; `--core-api 1.27` selects the historical official Hub
197
+ package set for older hosts.
198
+
199
+ `presave.js` and `upgrades.js` are detected but never executed implicitly.
200
+ They can contain migrations that semantics alone cannot express. Applications
201
+ that need those migrations must provide a sandboxed JavaScript adapter through
202
+ the optional `js` extra, then validate the migrated data.
203
+
204
+ Historical packages can be installed from local sources when the current Hub
205
+ no longer serves their versions. pyh5p does not rewrite a library's declared
206
+ Core API or claim that a newer runtime works on an older host.
207
+
208
+ ## Security
209
+
210
+ Archive reading and extraction reject traversal, symlinks, duplicate entries,
211
+ large archives, unsafe compression ratios, and disallowed extensions. Remote
212
+ downloads require HTTPS, an allowlisted host, and public DNS resolution.
213
+ Downloads happen only when fetching is enabled. Library JavaScript is never
214
+ executed by the Python package. Applications that display HTML or media must
215
+ apply their own sanitization and content policy.
216
+
217
+ ## Development
218
+
219
+ ```text
220
+ python -m pip install -e ".[dev]"
221
+ python -m pytest --basetemp .pytest-local
222
+ ruff check .
223
+ mypy src/pyh5p
224
+ python -m build
225
+ python -m twine check dist\*
226
+ ```
227
+
228
+ The checked-in tests cover semantics, validation paths, dependency graphs,
229
+ nested content, media, round trips, archive security, Hub package selection,
230
+ and malformed input. `scripts/discover_types.py` can fetch an opt-in local
231
+ registry corpus. `scripts/differential_check.py` can run an external validator
232
+ when one is configured. PHP, Lumi, WordPress, and Moodle are not dependencies
233
+ of the package or CI job.
234
+
235
+ Contributions should include a regression test for each fixed bug. Keep the
236
+ core clean-room and do not copy code from the GPL H5P PHP libraries.
237
+
238
+ ## License
239
+
240
+ The project is licensed under the [MIT License](LICENSE) and maintained by
241
+ [Nikita Berger](https://nikity.is-a.dev).
242
+
243
+ H5P is a trademark of H5P Group. pyh5p is independent and is not affiliated
244
+ with or endorsed by H5P Group, H5P.org, Lumi, Moodle, or WordPress. H5P
245
+ libraries retain their own licenses.
pyh5p-0.1.0/README.md ADDED
@@ -0,0 +1,186 @@
1
+ # pyh5p
2
+
3
+ `pyh5p` creates, edits, validates, and packages H5P content in Python. It
4
+ reads each library's `library.json` and `semantics.json`, so the Python code
5
+ does not need a class for every H5P content type.
6
+
7
+ ## Quick start
8
+
9
+ Install the package:
10
+
11
+ ```text
12
+ pip install pyh5p
13
+ ```
14
+
15
+ Put the content data in `answers.json`:
16
+
17
+ ```json
18
+ {
19
+ "question": "What is 2 + 2?",
20
+ "answers": [
21
+ {"text": "4", "correct": true},
22
+ {"text": "5", "correct": false}
23
+ ]
24
+ }
25
+ ```
26
+
27
+ Build the package:
28
+
29
+ ```text
30
+ pyh5p build H5P.MultiChoice answers.json question.h5p
31
+ ```
32
+
33
+ The command fetches a complete package and its dependencies from the official
34
+ H5P Content Type Hub when they are not already cached. Fetching is enabled by
35
+ default. `question.h5p` is ready to import into an H5P host.
36
+
37
+ For Lumi Desktop or Moodle installations that run Core API 1.27, request the
38
+ matching historical package set:
39
+
40
+ ```text
41
+ pyh5p build H5P.MultiChoice answers.json question.h5p --core-api 1.27
42
+ ```
43
+
44
+ ## Python API
45
+
46
+ The same operation is available without a template or content-type-specific
47
+ Python code:
48
+
49
+ ```python
50
+ import pyh5p
51
+
52
+ content = pyh5p.build(
53
+ "H5P.MultiChoice",
54
+ {
55
+ "question": "What is 2 + 2?",
56
+ "answers": [
57
+ {"text": "4", "correct": True},
58
+ {"text": "5", "correct": False},
59
+ ],
60
+ },
61
+ )
62
+ content.save("question.h5p")
63
+ ```
64
+
65
+ The field names and validation rules come from the installed library's
66
+ semantics. The same API works with other runnable libraries, including local,
67
+ historical, and third-party libraries.
68
+
69
+ Media uses the same generic path. For example:
70
+
71
+ ```python
72
+ content = pyh5p.build("H5P.Image", {"decorative": True}, validate=False)
73
+ content.import_media(
74
+ "file", b"image bytes", target="images/example.png", mime="image/png"
75
+ )
76
+ content.save("image.h5p")
77
+ ```
78
+
79
+ `LibraryStore` accepts local library directories and archives. Use it for
80
+ offline builds or for a controlled library set:
81
+
82
+ ```python
83
+ store = pyh5p.LibraryStore("libraries")
84
+ content = pyh5p.build("H5P.MultiChoice", data, store=store, fetch=False)
85
+ ```
86
+
87
+ ## What is generic
88
+
89
+ The semantics engine handles groups, lists, nested libraries, text and HTML,
90
+ numbers, booleans, selects, image/video/audio/file fields, defaults, optional
91
+ values, regular expressions, ranges, allowed tags, widgets, `showWhen`,
92
+ `isSubContent`, unknown attributes, and recursive dependencies.
93
+
94
+ It also supports dependency closure for preloaded, dynamic, editor, and
95
+ nested-content libraries. Packages can be loaded, edited, validated, and
96
+ exported again. Builds use stable JSON ordering and ZIP timestamps where the
97
+ H5P format allows it.
98
+
99
+ ## Command line
100
+
101
+ ```text
102
+ pyh5p build H5P.MultiChoice answers.json question.h5p
103
+ pyh5p inspect question.h5p
104
+ pyh5p validate question.h5p
105
+ pyh5p schema H5P.MultiChoice --libraries .pyh5p-libraries -o schema.json
106
+ pyh5p unpack question.h5p unpacked
107
+ ```
108
+
109
+ Use `--offline` with `build` when network access is not allowed. Use
110
+ `--cache-dir` to choose the library cache. `--content-only` is available when
111
+ the destination explicitly accepts a package without libraries.
112
+
113
+ To preview a package, import it into [Lumi Desktop](https://lumi.education/en/lumi-h5p-offline-desktop-editor/)
114
+ and open its preview. The H5P host runs the library JavaScript; pyh5p creates
115
+ the content and portable package.
116
+
117
+ ## JSON Schema and AI applications
118
+
119
+ Ask pyh5p for the schema, pass it to any provider's structured-output API,
120
+ then validate and build the returned object:
121
+
122
+ ```python
123
+ schema = pyh5p.schema_json("H5P.MultiChoice", store=store)
124
+ returned = provider.generate_json(schema=schema)
125
+ content = pyh5p.build("H5P.MultiChoice", returned, store=store)
126
+ content.require_valid()
127
+ ```
128
+
129
+ The core package has no dependency on an AI provider.
130
+
131
+ ## Compatibility and JavaScript hooks
132
+
133
+ Compatibility means that pyh5p parses the supplied definitions, validates the
134
+ semantic data, resolves dependencies, preserves distributable library files,
135
+ and writes the standard H5P package structure. The host must support the Core
136
+ API version declared by the selected libraries. Current Hub packages may
137
+ require Core API 1.28; `--core-api 1.27` selects the historical official Hub
138
+ package set for older hosts.
139
+
140
+ `presave.js` and `upgrades.js` are detected but never executed implicitly.
141
+ They can contain migrations that semantics alone cannot express. Applications
142
+ that need those migrations must provide a sandboxed JavaScript adapter through
143
+ the optional `js` extra, then validate the migrated data.
144
+
145
+ Historical packages can be installed from local sources when the current Hub
146
+ no longer serves their versions. pyh5p does not rewrite a library's declared
147
+ Core API or claim that a newer runtime works on an older host.
148
+
149
+ ## Security
150
+
151
+ Archive reading and extraction reject traversal, symlinks, duplicate entries,
152
+ large archives, unsafe compression ratios, and disallowed extensions. Remote
153
+ downloads require HTTPS, an allowlisted host, and public DNS resolution.
154
+ Downloads happen only when fetching is enabled. Library JavaScript is never
155
+ executed by the Python package. Applications that display HTML or media must
156
+ apply their own sanitization and content policy.
157
+
158
+ ## Development
159
+
160
+ ```text
161
+ python -m pip install -e ".[dev]"
162
+ python -m pytest --basetemp .pytest-local
163
+ ruff check .
164
+ mypy src/pyh5p
165
+ python -m build
166
+ python -m twine check dist\*
167
+ ```
168
+
169
+ The checked-in tests cover semantics, validation paths, dependency graphs,
170
+ nested content, media, round trips, archive security, Hub package selection,
171
+ and malformed input. `scripts/discover_types.py` can fetch an opt-in local
172
+ registry corpus. `scripts/differential_check.py` can run an external validator
173
+ when one is configured. PHP, Lumi, WordPress, and Moodle are not dependencies
174
+ of the package or CI job.
175
+
176
+ Contributions should include a regression test for each fixed bug. Keep the
177
+ core clean-room and do not copy code from the GPL H5P PHP libraries.
178
+
179
+ ## License
180
+
181
+ The project is licensed under the [MIT License](LICENSE) and maintained by
182
+ [Nikita Berger](https://nikity.is-a.dev).
183
+
184
+ H5P is a trademark of H5P Group. pyh5p is independent and is not affiliated
185
+ with or endorsed by H5P Group, H5P.org, Lumi, Moodle, or WordPress. H5P
186
+ libraries retain their own licenses.
@@ -0,0 +1,13 @@
1
+ {
2
+ "question": "What is 2 + 2?",
3
+ "answers": [
4
+ {
5
+ "text": "4",
6
+ "correct": true
7
+ },
8
+ {
9
+ "text": "5",
10
+ "correct": false
11
+ }
12
+ ]
13
+ }
@@ -0,0 +1,7 @@
1
+ """Edit an existing H5P package without executing its JavaScript."""
2
+
3
+ import pyh5p
4
+
5
+ content = pyh5p.load("input.h5p")
6
+ content.set("title", "Edited by pyh5p")
7
+ content.save("output.h5p")
@@ -0,0 +1,17 @@
1
+ # Test fixtures
2
+
3
+ `libraries/` contains small synthetic libraries used by the tests. They cover
4
+ the semantic field types, nested library content, dependency classes, media
5
+ fields, `showWhen`, unknown attributes, and JavaScript hook detection.
6
+
7
+ The official registry and Hub change independently of pyh5p releases, and
8
+ third-party repositories carry their own licenses. The discovery tool can
9
+ write an opt-in local corpus without adding those libraries to the repository:
10
+
11
+ ```text
12
+ python scripts/discover_types.py --output fixtures/downloaded --download
13
+ ```
14
+
15
+ Use the resulting local directories for compatibility and differential runs.
16
+ Do not commit downloaded libraries until their repository and asset licenses
17
+ permit redistribution.
@@ -0,0 +1,10 @@
1
+ {
2
+ "body": "<strong>Hello</strong><script>unsafe()</script>",
3
+ "nested": {
4
+ "library": "H5P.NestedFixture 1.0",
5
+ "params": {"text": "Nested content"}
6
+ },
7
+ "items": [{"text": "First item"}],
8
+ "options": {"future": "preserved"},
9
+ "unknownAllowed": "visible"
10
+ }
@@ -0,0 +1 @@
1
+ H5P.NestedFixture = function () {};
@@ -0,0 +1,9 @@
1
+ {
2
+ "title": "Nested fixture",
3
+ "machineName": "H5P.NestedFixture",
4
+ "majorVersion": 1,
5
+ "minorVersion": 0,
6
+ "patchVersion": 0,
7
+ "runnable": 1,
8
+ "preloadedDependencies": [{"machineName": "H5P.SyntheticDependency", "majorVersion": 1, "minorVersion": 0}]
9
+ }
@@ -0,0 +1,3 @@
1
+ [
2
+ {"name": "text", "type": "text", "label": "Text"}
3
+ ]
@@ -0,0 +1 @@
1
+ .synthetic-fixture { display: block; }
@@ -0,0 +1,2 @@
1
+ // Fixture JavaScript is intentionally never executed by pyh5p.
2
+ H5P.SyntheticAllTypes = function () {};
@@ -0,0 +1,17 @@
1
+ {
2
+ "title": "Synthetic semantics fixture",
3
+ "machineName": "H5P.SyntheticAllTypes",
4
+ "majorVersion": 1,
5
+ "minorVersion": 0,
6
+ "patchVersion": 0,
7
+ "runnable": 1,
8
+ "author": "Nikita Berger",
9
+ "license": "MIT",
10
+ "coreApi": {"majorVersion": 1, "minorVersion": 27},
11
+ "preloadedJs": [{"path": "js/synthetic.js"}],
12
+ "preloadedCss": [{"path": "css/synthetic.css"}],
13
+ "preloadedDependencies": [{"machineName": "H5P.SyntheticDependency", "majorVersion": 1, "minorVersion": 0}],
14
+ "dynamicDependencies": [{"machineName": "H5P.SyntheticDynamic", "majorVersion": 1, "minorVersion": 0}],
15
+ "editorDependencies": [{"machineName": "H5PEditor.SyntheticWidget", "majorVersion": 1, "minorVersion": 0}],
16
+ "futureAttribute": {"preserve": true}
17
+ }
@@ -0,0 +1,2 @@
1
+ // Fixture hook used to verify explicit hook detection.
2
+ function presave() {}
@@ -0,0 +1,15 @@
1
+ [
2
+ {"name": "title", "type": "text", "label": "Title", "default": "Example", "maxLength": 80, "regexp": {"pattern": "^[A-Za-z ]+$", "modifiers": "i"}},
3
+ {"name": "body", "type": "text", "label": "HTML body", "widget": "html", "tags": ["strong", "em", "a", "p"]},
4
+ {"name": "count", "type": "number", "label": "Count", "min": 0, "max": 100, "steps": 5, "decimals": 0, "default": 10},
5
+ {"name": "enabled", "type": "boolean", "label": "Enabled", "default": true},
6
+ {"name": "style", "type": "select", "label": "Style", "options": [{"value": "plain", "label": "Plain"}, {"value": "rich", "label": "Rich"}], "default": "plain"},
7
+ {"name": "nested", "type": "library", "label": "Nested activity", "options": ["H5P.NestedFixture 1.0"]},
8
+ {"name": "picture", "type": "image", "label": "Picture", "optional": true, "showWhen": {"rules": [{"field": "enabled", "equals": true}]}},
9
+ {"name": "movies", "type": "video", "label": "Movies", "optional": true},
10
+ {"name": "sounds", "type": "audio", "label": "Sounds", "optional": true},
11
+ {"name": "attachment", "type": "file", "label": "Attachment", "optional": true, "allowedExtensions": ["pdf", "txt"]},
12
+ {"name": "items", "type": "list", "label": "Items", "entity": "item", "min": 1, "max": 3, "defaultNum": 1, "field": {"name": "item", "type": "group", "label": "Item", "isSubContent": true, "fields": [{"name": "text", "type": "text", "label": "Text"}, {"name": "flag", "type": "boolean", "label": "Flag", "default": false}]}},
13
+ {"name": "options", "type": "group", "label": "Options", "expanded": true, "fields": [{"name": "optionalText", "type": "text", "label": "Optional", "optional": true}, {"name": "future", "type": "future-type", "label": "Future", "newAttribute": "keep-me"}]},
14
+ {"name": "unknownAllowed", "type": "text", "label": "Unknown attribute", "widget": "showWhen", "showWhen": {"type": "and", "rules": [{"field": "enabled", "equals": true}], "detach": true, "customRuleAttribute": 7}}
15
+ ]
@@ -0,0 +1,2 @@
1
+ // Fixture migration hook used to verify explicit hook detection.
2
+ function upgrades() {}
@@ -0,0 +1 @@
1
+ H5P.SyntheticDependency = {};
@@ -0,0 +1,9 @@
1
+ {
2
+ "title": "Synthetic dependency",
3
+ "machineName": "H5P.SyntheticDependency",
4
+ "majorVersion": 1,
5
+ "minorVersion": 0,
6
+ "patchVersion": 2,
7
+ "runnable": 0,
8
+ "coreApi": {"majorVersion": 1, "minorVersion": 0}
9
+ }
@@ -0,0 +1 @@
1
+ H5P.SyntheticDynamic = {};