mdq 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. mdq-0.1.0/LICENSE +21 -0
  2. mdq-0.1.0/PKG-INFO +168 -0
  3. mdq-0.1.0/README.md +148 -0
  4. mdq-0.1.0/mdq/__init__.py +19 -0
  5. mdq-0.1.0/mdq/__main__.py +4 -0
  6. mdq-0.1.0/mdq/_diagnostics.py +40 -0
  7. mdq-0.1.0/mdq/cli.py +327 -0
  8. mdq-0.1.0/mdq/convert/__init__.py +14 -0
  9. mdq-0.1.0/mdq/convert/aiken.py +151 -0
  10. mdq-0.1.0/mdq/convert/base.py +106 -0
  11. mdq-0.1.0/mdq/convert/gift.py +876 -0
  12. mdq-0.1.0/mdq/convert/moodle_xml.py +922 -0
  13. mdq-0.1.0/mdq/convert/parser.py +161 -0
  14. mdq-0.1.0/mdq/errors.py +114 -0
  15. mdq-0.1.0/mdq/grading.py +87 -0
  16. mdq-0.1.0/mdq/hypothesis/__init__.py +14 -0
  17. mdq-0.1.0/mdq/hypothesis/aiken.py +59 -0
  18. mdq-0.1.0/mdq/hypothesis/documents.py +625 -0
  19. mdq-0.1.0/mdq/hypothesis/frontmatter.py +88 -0
  20. mdq-0.1.0/mdq/hypothesis/gift.py +140 -0
  21. mdq-0.1.0/mdq/hypothesis/moodle_xml.py +234 -0
  22. mdq-0.1.0/mdq/hypothesis/schedule.py +236 -0
  23. mdq-0.1.0/mdq/hypothesis/slugs.py +247 -0
  24. mdq-0.1.0/mdq/linter.py +905 -0
  25. mdq-0.1.0/mdq/loaders.py +113 -0
  26. mdq-0.1.0/mdq/loading.py +520 -0
  27. mdq-0.1.0/mdq/mdit/__init__.py +0 -0
  28. mdq-0.1.0/mdq/mdit/plugins/__init__.py +0 -0
  29. mdq-0.1.0/mdq/mdit/plugins/choices.py +170 -0
  30. mdq-0.1.0/mdq/mdq.schema.json +1172 -0
  31. mdq-0.1.0/mdq/models.py +1836 -0
  32. mdq-0.1.0/mdq/parser.py +2101 -0
  33. mdq-0.1.0/mdq/py.typed +0 -0
  34. mdq-0.1.0/mdq/regex.py +238 -0
  35. mdq-0.1.0/mdq/render.py +167 -0
  36. mdq-0.1.0/mdq/scaffold.py +438 -0
  37. mdq-0.1.0/mdq/schedule.py +188 -0
  38. mdq-0.1.0/mdq/scripts/__init__.py +0 -0
  39. mdq-0.1.0/mdq/scripts/lint_snapshot.py +159 -0
  40. mdq-0.1.0/mdq/show.py +534 -0
  41. mdq-0.1.0/mdq/slugify.py +221 -0
  42. mdq-0.1.0/mdq/testing.py +129 -0
  43. mdq-0.1.0/mdq/types.py +403 -0
  44. mdq-0.1.0/pyproject.toml +72 -0
  45. mdq-0.1.0/pyproject.toml.orig +73 -0
mdq-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fábio Macêdo Mendes
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.
mdq-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,168 @@
1
+ Metadata-Version: 2.4
2
+ Name: mdq
3
+ Version: 0.1.0
4
+ Summary: Reference schemas and validation CLI for the MDQ question file format.
5
+ Author: Fábio Macêdo Mendes
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Requires-Dist: pyyaml>=6.0
9
+ Requires-Dist: typer>=0.12
10
+ Requires-Dist: rich>=15.0.0
11
+ Requires-Dist: markdown-it-py>=4.2.0
12
+ Requires-Dist: pydantic>=2.9
13
+ Requires-Dist: optionz>=0.2.0
14
+ Requires-Dist: linkify-it-py>=2.2.0
15
+ Requires-Dist: python-slugify>=8.0.4
16
+ Requires-Dist: pytest>=7 ; extra == 'test'
17
+ Requires-Python: >=3.13
18
+ Provides-Extra: test
19
+ Description-Content-Type: text/markdown
20
+
21
+ # mdq-py
22
+
23
+ [![CI](https://github.com/fabiommendes/mdq-spec/actions/workflows/ci.yml/badge.svg)](https://github.com/fabiommendes/mdq-spec/actions/workflows/ci.yml)
24
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
25
+
26
+ > [!WARNING]
27
+ > This is a work in progress. The MDQ specification is not stable yet, and the
28
+ > API and the CLI may change.
29
+
30
+ Python reference implementation of [MDQ](https://github.com/fabiommendes/mdq-spec),
31
+ a file format to write questions and exams in plain Markdown:
32
+
33
+ ```md
34
+ What is the capital of Brazil?
35
+
36
+ * [ ] Rio de Janeiro
37
+ > It was the capital until 1960.
38
+ * [*] Brasília
39
+ * [ ] São Paulo
40
+ ```
41
+
42
+ `mdq-py` provides:
43
+
44
+ * A parser from MDQ Markdown (and from YAML/JSON) to Pydantic models.
45
+ * A linter that reports problems the schema cannot express.
46
+ * Scoring of student responses.
47
+ * Converters to and from GIFT, Aiken and Moodle XML.
48
+ * The `mdq` command line tool.
49
+
50
+ The syntax of each question type is in the
51
+ [specification](https://github.com/fabiommendes/mdq-spec/tree/main/docs).
52
+
53
+
54
+ ## Installation
55
+
56
+ Requires Python 3.13 or later. The package is not on PyPI yet. Install it from
57
+ the repository:
58
+
59
+ ```bash
60
+ # CLI only
61
+ uv tool install "git+https://github.com/fabiommendes/mdq-py"
62
+
63
+ # As a library
64
+ uv add "git+https://github.com/fabiommendes/mdq-py"
65
+ ```
66
+
67
+
68
+ ## Command line
69
+
70
+ ```bash
71
+ mdq new multiple-choice # scaffold multiple-choice.mdq.md
72
+ mdq new numeric --complete # scaffold with every feature of the type
73
+ mdq validate question.mdq.md # print errors and warnings
74
+ mdq validate question.mdq.md --level strict # also print info diagnostics
75
+ mdq show exam.mdq.md # render the parsed document
76
+ mdq show exam.mdq.md --no-answer-key # preview it as a student sees it
77
+ mdq export question.mdq.md --format gift
78
+ mdq import question.gift -o question.mdq.md
79
+ ```
80
+
81
+ `mdq validate` exits with a non-zero code if the document has errors:
82
+
83
+ ```
84
+ $ mdq validate capital.mdq.md
85
+ OK capital.mdq.md
86
+ ```
87
+
88
+ `import` and `export` support `aiken`, `gift` and `moodle-xml`. The format is
89
+ inferred from the file extension if `--format` is not given. Run `mdq --help`
90
+ or `mdq <command> --help` for all options.
91
+
92
+
93
+ ## Library
94
+
95
+ `mdq.parse()` returns a validated model, or raises `InvalidDocument`:
96
+
97
+ ```python
98
+ from pathlib import Path
99
+
100
+ import mdq
101
+
102
+ question = mdq.parse("""
103
+ What is the capital of Brazil?
104
+
105
+ * [ ] Rio de Janeiro
106
+ * [*] Brasília
107
+ """)
108
+
109
+ print(type(question).__name__) # MultipleChoiceQuestion
110
+ print(question.to_dict())
111
+ ```
112
+
113
+ A `str` is read as MDQ source. Pass a `Path` to read a file, or a mapping to
114
+ load an already parsed document. The `format` argument selects `"mdq"`,
115
+ `"yaml"` or `"json"` explicitly.
116
+
117
+ `mdq.load()` never raises for a problem in the document. It returns the
118
+ document together with every diagnostic, which is useful for editors and
119
+ linters:
120
+
121
+ ```python
122
+ loaded = mdq.load(Path("exam.mdq.md"))
123
+ for diagnostic in loaded.diagnostics:
124
+ print(diagnostic.severity, diagnostic.code, diagnostic.message)
125
+
126
+ exam = loaded.validate(raise_on="warning")
127
+ ```
128
+
129
+ Other useful parts of the API:
130
+
131
+ * `ids="fill"` in `parse()`/`load()`, or `document.with_ids()`, derives the
132
+ missing ids of questions and choices.
133
+ * `question.score_response(response)` scores a student response.
134
+ * `question.render()` serializes a model back to MDQ Markdown.
135
+ * `Exam.resolve()` replaces the `include` blocks of an exam with the questions
136
+ they refer to.
137
+ * `mdq.convert.import_question()` and `mdq.convert.export_question()` convert
138
+ from and to other formats.
139
+
140
+
141
+ ## Development
142
+
143
+ The test suite reads the specification, the schema and the shared examples from
144
+ the parent [mdq-spec](https://github.com/fabiommendes/mdq-spec) repository, where
145
+ this package lives in the `mdq-py/` directory. Clone that repository to run the
146
+ tests; a standalone clone of `mdq-py` cannot run them.
147
+
148
+ ```bash
149
+ git clone https://github.com/fabiommendes/mdq-spec
150
+ cd mdq-spec/mdq-py
151
+ uv sync
152
+ uv run pytest # tests
153
+ uv run mypy mdq # type check
154
+ uv run ruff check # lint
155
+ ```
156
+
157
+ This is the reference implementation: new features are specified in
158
+ `mdq-spec`, implemented here, and then ported to
159
+ [mdq-js](https://github.com/fabiommendes/mdq-js). See [AGENTS.md](AGENTS.md) for
160
+ the coding conventions.
161
+
162
+ See the [changelog](CHANGELOG.md) and the
163
+ [contributing guide](https://github.com/fabiommendes/mdq-spec/blob/main/CONTRIBUTING.md).
164
+
165
+
166
+ ## License
167
+
168
+ [MIT](LICENSE)
mdq-0.1.0/README.md ADDED
@@ -0,0 +1,148 @@
1
+ # mdq-py
2
+
3
+ [![CI](https://github.com/fabiommendes/mdq-spec/actions/workflows/ci.yml/badge.svg)](https://github.com/fabiommendes/mdq-spec/actions/workflows/ci.yml)
4
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
+
6
+ > [!WARNING]
7
+ > This is a work in progress. The MDQ specification is not stable yet, and the
8
+ > API and the CLI may change.
9
+
10
+ Python reference implementation of [MDQ](https://github.com/fabiommendes/mdq-spec),
11
+ a file format to write questions and exams in plain Markdown:
12
+
13
+ ```md
14
+ What is the capital of Brazil?
15
+
16
+ * [ ] Rio de Janeiro
17
+ > It was the capital until 1960.
18
+ * [*] Brasília
19
+ * [ ] São Paulo
20
+ ```
21
+
22
+ `mdq-py` provides:
23
+
24
+ * A parser from MDQ Markdown (and from YAML/JSON) to Pydantic models.
25
+ * A linter that reports problems the schema cannot express.
26
+ * Scoring of student responses.
27
+ * Converters to and from GIFT, Aiken and Moodle XML.
28
+ * The `mdq` command line tool.
29
+
30
+ The syntax of each question type is in the
31
+ [specification](https://github.com/fabiommendes/mdq-spec/tree/main/docs).
32
+
33
+
34
+ ## Installation
35
+
36
+ Requires Python 3.13 or later. The package is not on PyPI yet. Install it from
37
+ the repository:
38
+
39
+ ```bash
40
+ # CLI only
41
+ uv tool install "git+https://github.com/fabiommendes/mdq-py"
42
+
43
+ # As a library
44
+ uv add "git+https://github.com/fabiommendes/mdq-py"
45
+ ```
46
+
47
+
48
+ ## Command line
49
+
50
+ ```bash
51
+ mdq new multiple-choice # scaffold multiple-choice.mdq.md
52
+ mdq new numeric --complete # scaffold with every feature of the type
53
+ mdq validate question.mdq.md # print errors and warnings
54
+ mdq validate question.mdq.md --level strict # also print info diagnostics
55
+ mdq show exam.mdq.md # render the parsed document
56
+ mdq show exam.mdq.md --no-answer-key # preview it as a student sees it
57
+ mdq export question.mdq.md --format gift
58
+ mdq import question.gift -o question.mdq.md
59
+ ```
60
+
61
+ `mdq validate` exits with a non-zero code if the document has errors:
62
+
63
+ ```
64
+ $ mdq validate capital.mdq.md
65
+ OK capital.mdq.md
66
+ ```
67
+
68
+ `import` and `export` support `aiken`, `gift` and `moodle-xml`. The format is
69
+ inferred from the file extension if `--format` is not given. Run `mdq --help`
70
+ or `mdq <command> --help` for all options.
71
+
72
+
73
+ ## Library
74
+
75
+ `mdq.parse()` returns a validated model, or raises `InvalidDocument`:
76
+
77
+ ```python
78
+ from pathlib import Path
79
+
80
+ import mdq
81
+
82
+ question = mdq.parse("""
83
+ What is the capital of Brazil?
84
+
85
+ * [ ] Rio de Janeiro
86
+ * [*] Brasília
87
+ """)
88
+
89
+ print(type(question).__name__) # MultipleChoiceQuestion
90
+ print(question.to_dict())
91
+ ```
92
+
93
+ A `str` is read as MDQ source. Pass a `Path` to read a file, or a mapping to
94
+ load an already parsed document. The `format` argument selects `"mdq"`,
95
+ `"yaml"` or `"json"` explicitly.
96
+
97
+ `mdq.load()` never raises for a problem in the document. It returns the
98
+ document together with every diagnostic, which is useful for editors and
99
+ linters:
100
+
101
+ ```python
102
+ loaded = mdq.load(Path("exam.mdq.md"))
103
+ for diagnostic in loaded.diagnostics:
104
+ print(diagnostic.severity, diagnostic.code, diagnostic.message)
105
+
106
+ exam = loaded.validate(raise_on="warning")
107
+ ```
108
+
109
+ Other useful parts of the API:
110
+
111
+ * `ids="fill"` in `parse()`/`load()`, or `document.with_ids()`, derives the
112
+ missing ids of questions and choices.
113
+ * `question.score_response(response)` scores a student response.
114
+ * `question.render()` serializes a model back to MDQ Markdown.
115
+ * `Exam.resolve()` replaces the `include` blocks of an exam with the questions
116
+ they refer to.
117
+ * `mdq.convert.import_question()` and `mdq.convert.export_question()` convert
118
+ from and to other formats.
119
+
120
+
121
+ ## Development
122
+
123
+ The test suite reads the specification, the schema and the shared examples from
124
+ the parent [mdq-spec](https://github.com/fabiommendes/mdq-spec) repository, where
125
+ this package lives in the `mdq-py/` directory. Clone that repository to run the
126
+ tests; a standalone clone of `mdq-py` cannot run them.
127
+
128
+ ```bash
129
+ git clone https://github.com/fabiommendes/mdq-spec
130
+ cd mdq-spec/mdq-py
131
+ uv sync
132
+ uv run pytest # tests
133
+ uv run mypy mdq # type check
134
+ uv run ruff check # lint
135
+ ```
136
+
137
+ This is the reference implementation: new features are specified in
138
+ `mdq-spec`, implemented here, and then ported to
139
+ [mdq-js](https://github.com/fabiommendes/mdq-js). See [AGENTS.md](AGENTS.md) for
140
+ the coding conventions.
141
+
142
+ See the [changelog](CHANGELOG.md) and the
143
+ [contributing guide](https://github.com/fabiommendes/mdq-spec/blob/main/CONTRIBUTING.md).
144
+
145
+
146
+ ## License
147
+
148
+ [MIT](LICENSE)
@@ -0,0 +1,19 @@
1
+ """mdq: tools for the MDQ (Markdown Questions) file format."""
2
+
3
+ from .errors import ParseError
4
+ from .loading import Diagnostic, InvalidDocument, Loaded, Severity, load, parse
5
+ from .models import Exam, Question
6
+
7
+ __version__ = "0.1.0"
8
+ __all__ = [
9
+ "Question",
10
+ "Exam",
11
+ "ParseError",
12
+ #: Loading
13
+ "load",
14
+ "parse",
15
+ "Loaded",
16
+ "Diagnostic",
17
+ "Severity",
18
+ "InvalidDocument",
19
+ ]
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,40 @@
1
+ """
2
+ `Diagnostic`, the one problem shape every stage of `mdq.loading`'s
3
+ pipeline reports through -- a parse failure, a pydantic error, a lint
4
+ warning, an unknown frontmatter key.
5
+
6
+ Split out from `mdq.loading` (which owns and re-exports it) so that
7
+ `mdq.linter` and `mdq.parser` -- both upstream of `mdq.loading` in the
8
+ pipeline -- can report `Diagnostic`s of their own without importing back
9
+ into the module that orchestrates them.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from dataclasses import dataclass
15
+ from typing import Literal
16
+
17
+ Severity = Literal["error", "warning", "info"]
18
+
19
+ #: Severity, ranked so "at or above" (as `Loaded.validate` uses it) is a
20
+ #: plain integer comparison. Higher is more severe.
21
+ RANK: dict[Severity, int] = {"info": 1, "warning": 2, "error": 3}
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class Diagnostic:
26
+ """
27
+ One problem found while loading a document.
28
+
29
+ `path` locates the offending value the way a `jsonschema`
30
+ `ValidationError.path` or a pydantic error's `loc` does: a tuple of
31
+ keys/indices from the document's root. `line` is set only for a
32
+ parse failure tied to a source node, and only when that node's
33
+ position in the source is known.
34
+ """
35
+
36
+ severity: Severity
37
+ code: str
38
+ message: str
39
+ path: tuple[str | int, ...] = ()
40
+ line: int | None = None