From a9f286fca082378063294616acbe51d26ba9255d Mon Sep 17 00:00:00 2001 From: Ethan Davidson <31261035+EthanThatOneKid@users.noreply.github.com> Date: Sat, 4 Jul 2026 15:43:37 -0700 Subject: [PATCH 01/10] Implement initial LMD Core parser --- .github/workflows/ci.yml | 19 ++ README.md | 11 + pyproject.toml | 30 ++ src/linked_markdown/__init__.py | 13 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 452 bytes .../__pycache__/links.cpython-312.pyc | Bin 0 -> 2444 bytes .../__pycache__/parser.cpython-312.pyc | Bin 0 -> 4791 bytes .../__pycache__/rdf.cpython-312.pyc | Bin 0 -> 2508 bytes .../__pycache__/types.cpython-312.pyc | Bin 0 -> 2117 bytes src/linked_markdown/links.py | 42 +++ src/linked_markdown/parser.py | 85 +++++ src/linked_markdown/rdf.py | 43 +++ src/linked_markdown/types.py | 41 +++ ...t_conformance.cpython-312-pytest-9.1.1.pyc | Bin 0 -> 6991 bytes tests/test_conformance.py | 37 +++ uv.lock | 312 ++++++++++++++++++ 16 files changed, 633 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 pyproject.toml create mode 100644 src/linked_markdown/__init__.py create mode 100644 src/linked_markdown/__pycache__/__init__.cpython-312.pyc create mode 100644 src/linked_markdown/__pycache__/links.cpython-312.pyc create mode 100644 src/linked_markdown/__pycache__/parser.cpython-312.pyc create mode 100644 src/linked_markdown/__pycache__/rdf.cpython-312.pyc create mode 100644 src/linked_markdown/__pycache__/types.cpython-312.pyc create mode 100644 src/linked_markdown/links.py create mode 100644 src/linked_markdown/parser.py create mode 100644 src/linked_markdown/rdf.py create mode 100644 src/linked_markdown/types.py create mode 100644 tests/__pycache__/test_conformance.cpython-312-pytest-9.1.1.pyc create mode 100644 tests/test_conformance.py create mode 100644 uv.lock diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2648e92 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,19 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - uses: astral-sh/setup-uv@v6 + + - run: uv run pytest diff --git a/README.md b/README.md index b9d2d50..59c9f6c 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,20 @@ Python implementation of Linked Markdown, packaged for PyPI. +## API + +```python +from linked_markdown import parse, to_graph + +doc = parse(markdown) +graph = to_graph(doc) +``` + ## Development ```sh git submodule update --init --recursive uv run pytest ``` + +The conformance suite is consumed from the `wazootech/linked-markdown` spec repository as a git submodule. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..30cd17e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,30 @@ +[project] +name = "linked-markdown" +version = "0.1.0" +description = "Python implementation of Linked Markdown" +readme = "README.md" +requires-python = ">=3.11" +license = "MIT" +authors = [ + { name = "Wazoo Technology" } +] +dependencies = [ + "pydantic>=2.11.0", + "pyyaml>=6.0.0", + "rdflib>=7.1.0" +] + +[dependency-groups] +dev = [ + "pytest>=8.4.0" +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/linked_markdown"] + +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/src/linked_markdown/__init__.py b/src/linked_markdown/__init__.py new file mode 100644 index 0000000..6b33ff4 --- /dev/null +++ b/src/linked_markdown/__init__.py @@ -0,0 +1,13 @@ +from .parser import LmdError, parse +from .rdf import to_graph, to_ntriples, to_triples +from .types import LmpDocument, LmpLink + +__all__ = [ + "LmdError", + "LmpDocument", + "LmpLink", + "parse", + "to_graph", + "to_ntriples", + "to_triples", +] diff --git a/src/linked_markdown/__pycache__/__init__.cpython-312.pyc b/src/linked_markdown/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..823f768631f69a3c1e6c17b7b14e5de6fdddc19e GIT binary patch literal 452 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 E2RPbfbpQYW literal 0 HcmV?d00001 diff --git a/src/linked_markdown/__pycache__/rdf.cpython-312.pyc b/src/linked_markdown/__pycache__/rdf.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..94eea48f4c6100490b9a05e86cfe4707feb577b2 GIT binary patch literal 2508 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 list[LmpLink]: + masked = _CODE_SPAN.sub(lambda match: " " * len(match.group(0)), body) + links: list[LmpLink] = [] + + for match in _MARKDOWN_LINK.finditer(masked): + raw = match.group(0) + text = match.group(1) + target = match.group(2) + links.append( + LmpLink( + link_type="markdown", + target=target, + text=text, + span=(match.start(), match.start() + len(raw)), + is_external=bool(_EXTERNAL_TARGET.match(target)), + ) + ) + + for match in _WIKILINK.finditer(masked): + raw = match.group(0) + text = match.group(1) + links.append( + LmpLink( + link_type="wikilink", + target=text, + text=text, + span=(match.start(), match.start() + len(raw)), + is_external=bool(_EXTERNAL_TARGET.match(text)), + ) + ) + + return sorted(links, key=lambda link: link.span[0]) diff --git a/src/linked_markdown/parser.py b/src/linked_markdown/parser.py new file mode 100644 index 0000000..1a2f821 --- /dev/null +++ b/src/linked_markdown/parser.py @@ -0,0 +1,85 @@ +from pathlib import Path +from typing import Any + +import yaml + +from .links import extract_links +from .types import LmpDocument + + +class LmdError(Exception): + def __init__(self, code: str, message: str) -> None: + super().__init__(message) + self.code = code + + +def parse(content: str, base_iri: str | None = None, file_path: Path | None = None) -> LmpDocument: + del base_iri, file_path + normalized = content.replace("\r\n", "\n") + frontmatter, body = _extract_frontmatter(normalized) + context = _normalize_context(frontmatter.get("@context")) + doc_id = _read_string(frontmatter.get("@id") or frontmatter.get("id")) + + if not doc_id: + raise LmdError("LMD_MISSING_ID", "Expected id or @id in frontmatter.") + + raw_types = frontmatter.get("@type", frontmatter.get("type")) + if raw_types is None: + raise LmdError("LMD_MISSING_TYPE", "Expected @type or type in frontmatter.") + + types = [_expand_curie(item, context) for item in _normalize_string_array(raw_types)] + + return LmpDocument( + id=doc_id, + types=types, + context=context, + frontmatter=frontmatter, + body=body, + links=extract_links(body), + ) + + +def _extract_frontmatter(content: str) -> tuple[dict[str, Any], str]: + if not content.startswith("---\n"): + raise LmdError("LMD_MISSING_FRONTMATTER", "Expected frontmatter delimited by --- at the start of the document.") + + end = content.find("\n---", 4) + if end == -1: + raise LmdError("LMD_MISSING_FRONTMATTER", "Expected closing frontmatter delimiter.") + + raw_frontmatter = content[4:end] + body = content[end + 4 :] + body = body.lstrip("\n") + + parsed = yaml.safe_load(raw_frontmatter) or {} + if not isinstance(parsed, dict): + raise LmdError("LMD_INVALID_FRONTMATTER", "Frontmatter must parse to an object.") + + return parsed, body + + +def _normalize_context(value: Any) -> dict[str, str]: + if not isinstance(value, dict): + return {} + return {str(key): prefix for key, prefix in value.items() if isinstance(prefix, str)} + + +def _normalize_string_array(value: Any) -> list[str]: + if isinstance(value, str): + return [value] + if isinstance(value, list) and all(isinstance(item, str) for item in value): + return value + raise LmdError("LMD_INVALID_TYPE", "Expected @type or type to be a string or string array.") + + +def _read_string(value: Any) -> str | None: + return value if isinstance(value, str) and value else None + + +def _expand_curie(value: str, context: dict[str, str]) -> str: + if "://" in value: + return value + prefix, separator, suffix = value.partition(":") + if not separator: + return value + return f"{context[prefix]}{suffix}" if prefix in context else value diff --git a/src/linked_markdown/rdf.py b/src/linked_markdown/rdf.py new file mode 100644 index 0000000..42f0f13 --- /dev/null +++ b/src/linked_markdown/rdf.py @@ -0,0 +1,43 @@ +from collections.abc import Generator +from typing import Any + +from rdflib import Graph, Literal, URIRef +from rdflib.namespace import RDF + +from .parser import _expand_curie +from .types import LmpDocument + +ARTICLE_BODY = URIRef("https://schema.org/articleBody") + + +def to_triples(doc: LmpDocument) -> Generator[tuple[URIRef, URIRef, Any], None, None]: + if not doc.id: + return + + subject = URIRef(doc.id) + + for doc_type in doc.types: + yield subject, RDF.type, URIRef(doc_type) + + if doc.body: + yield subject, ARTICLE_BODY, Literal(doc.body) + + for key, value in doc.frontmatter.items(): + if key in {"id", "@id", "type", "@type", "@context"}: + continue + if not isinstance(value, str): + continue + yield subject, URIRef(_expand_curie(key, doc.context)), Literal(value) + + +def to_graph(doc: LmpDocument, graph: Graph | None = None) -> Graph: + target = graph or Graph() + for triple in to_triples(doc): + target.add(triple) + return target + + +def to_ntriples(doc: LmpDocument) -> str: + graph = to_graph(doc) + lines = graph.serialize(format="nt").splitlines() + return "\n".join(sorted(line for line in lines if line)) + "\n" diff --git a/src/linked_markdown/types.py b/src/linked_markdown/types.py new file mode 100644 index 0000000..e2b76d7 --- /dev/null +++ b/src/linked_markdown/types.py @@ -0,0 +1,41 @@ +from typing import Any, Literal + +from pydantic import BaseModel, ConfigDict + + +class LmpLink(BaseModel): + model_config = ConfigDict(populate_by_name=True) + + link_type: Literal["wikilink", "markdown"] + target: str + text: str + span: tuple[int, int] + is_external: bool + + def conformance_dict(self) -> dict[str, Any]: + return { + "linkType": self.link_type, + "target": self.target, + "text": self.text, + "span": list(self.span), + "isExternal": self.is_external, + } + + +class LmpDocument(BaseModel): + id: str | None + types: list[str] + context: dict[str, str] + frontmatter: dict[str, Any] + body: str + links: list[LmpLink] + + def conformance_dict(self) -> dict[str, Any]: + return { + "id": self.id, + "types": self.types, + "context": self.context, + "frontmatter": self.frontmatter, + "body": self.body, + "links": [link.conformance_dict() for link in self.links], + } diff --git a/tests/__pycache__/test_conformance.cpython-312-pytest-9.1.1.pyc b/tests/__pycache__/test_conformance.cpython-312-pytest-9.1.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..713ce93df92f1974618f140833569ed718be3ec8 GIT binary patch literal 6991 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 T#B;mmQ_Bd7h19=DK^^@MANFfj literal 0 HcmV?d00001 diff --git a/tests/test_conformance.py b/tests/test_conformance.py new file mode 100644 index 0000000..99f4e94 --- /dev/null +++ b/tests/test_conformance.py @@ -0,0 +1,37 @@ +import json +from pathlib import Path + +import pytest + +from linked_markdown import LmdError, parse, to_ntriples + +CONFORMANCE_ROOT = Path(__file__).parent.parent / "test" / "linked-markdown-spec" / "conformance" +MANIFEST = json.loads((CONFORMANCE_ROOT / "manifest.json").read_text()) + + +@pytest.mark.parametrize("case", MANIFEST["cases"], ids=lambda case: case["id"]) +def test_conformance(case: dict) -> None: + input_text = _read_fixture(case["input"]) + + if "expectError" in case: + with pytest.raises(LmdError) as error: + parse(input_text) + assert error.value.code == case["expectError"]["code"] + return + + doc = parse(input_text) + + if parsed_path := case.get("expect", {}).get("parsed"): + assert doc.conformance_dict() == json.loads(_read_fixture(parsed_path)) + + if rdf_path := case.get("expect", {}).get("rdf"): + assert _normalize_triples(to_ntriples(doc)) == _normalize_triples(_read_fixture(rdf_path)) + + +def _read_fixture(path: str) -> str: + return (CONFORMANCE_ROOT / path).read_text() + + +def _normalize_triples(value: str) -> str: + lines = [line for line in value.replace("\r\n", "\n").strip().split("\n") if line] + return "\n".join(sorted(lines)) + "\n" diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..00c2df4 --- /dev/null +++ b/uv.lock @@ -0,0 +1,312 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "linked-markdown" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "rdflib" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [ + { name = "pydantic", specifier = ">=2.11.0" }, + { name = "pyyaml", specifier = ">=6.0.0" }, + { name = "rdflib", specifier = ">=7.1.0" }, +] + +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = ">=8.4.0" }] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, + { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, + { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, + { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, + { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, + { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, + { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "rdflib" +version = "7.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyparsing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/f5/18bb77b7af9526add0c727a3b2048959847dc5fb030913e2918bf384fec3/rdflib-7.6.0.tar.gz", hash = "sha256:6c831288d5e4a5a7ece85d0ccde9877d512a3d0f02d7c06455d00d6d0ea379df", size = 4943826, upload-time = "2026-02-13T07:15:55.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/c2/6604a71269e0c1bd75656d5a001432d16f2cc5b8c057140ec797155c295e/rdflib-7.6.0-py3-none-any.whl", hash = "sha256:30c0a3ebf4c0e09215f066be7246794b6492e054e782d7ac2a34c9f70a15e0dd", size = 615416, upload-time = "2026-02-13T07:15:46.487Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +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" } +wheels = [ + { 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" }, +] From 72bdf1b9d19068acd98f0df052e4e3e79b797312 Mon Sep 17 00:00:00 2001 From: Ethan Davidson <31261035+EthanThatOneKid@users.noreply.github.com> Date: Sat, 4 Jul 2026 15:43:59 -0700 Subject: [PATCH 02/10] Ignore generated Python files --- .gitignore | 7 +++++++ .../__pycache__/__init__.cpython-312.pyc | Bin 452 -> 0 bytes .../__pycache__/links.cpython-312.pyc | Bin 2444 -> 0 bytes .../__pycache__/parser.cpython-312.pyc | Bin 4791 -> 0 bytes .../__pycache__/rdf.cpython-312.pyc | Bin 2508 -> 0 bytes .../__pycache__/types.cpython-312.pyc | Bin 2117 -> 0 bytes ...test_conformance.cpython-312-pytest-9.1.1.pyc | Bin 6991 -> 0 bytes 7 files changed, 7 insertions(+) create mode 100644 .gitignore delete mode 100644 src/linked_markdown/__pycache__/__init__.cpython-312.pyc delete mode 100644 src/linked_markdown/__pycache__/links.cpython-312.pyc delete mode 100644 src/linked_markdown/__pycache__/parser.cpython-312.pyc delete mode 100644 src/linked_markdown/__pycache__/rdf.cpython-312.pyc delete mode 100644 src/linked_markdown/__pycache__/types.cpython-312.pyc delete mode 100644 tests/__pycache__/test_conformance.cpython-312-pytest-9.1.1.pyc diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cb43dcf --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.venv/ +__pycache__/ +*.py[cod] +.pytest_cache/ +dist/ +build/ +*.egg-info/ diff --git a/src/linked_markdown/__pycache__/__init__.cpython-312.pyc b/src/linked_markdown/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 823f768631f69a3c1e6c17b7b14e5de6fdddc19e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 452 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 E2RPbfbpQYW diff --git a/src/linked_markdown/__pycache__/rdf.cpython-312.pyc b/src/linked_markdown/__pycache__/rdf.cpython-312.pyc deleted file mode 100644 index 94eea48f4c6100490b9a05e86cfe4707feb577b2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2508 zcmZuyU2IfE6rS08@9uW*{?K27Xj2dj8^TtC)*osGxBOJGHMB)smo%51*|N+1%iLS+ zHru2jBo-l|L}Gp5p(Z@ELE}$iB=JR~4<`DsWmTF9Bp5ZyTSJ zqZq+V&3c!dP@h@oawj}PflBbJF_6hJgY!q@TgE15W{qcIYpm?mfl zeo|iA1Y1NU>N1y?N%WkY*+4dUnlY*nbu0uL5vt^aBz{iG)YtB40Nn)WHkQ<6{XOwO z67#x4)TB&vEd~o|zgbF3Tsg!v`xFlyDY&qu^Wfmoo})A`6pnTuyeDh}J~l*0ZOtS@ zxe->NAW)AOcDI!qF{tT|wVrZa+j+6Q-N~JzhL*C}aJ$CbLQbczSfs=yV&!;{QFnxy zy}6pZ!X_jF{3QIBegf%h2t$y|s3>;Uk%LRfm3-l&b^Jh%W7r2M*Hbq-(T31?pp4z1 zzgP4j_60Dnc_-lCnU_Am(*XO$t+KGtOPMuCfMu<4Z`7B$37>nk_WFB4YlDAcbuKop z@;-OH#(+165y}R@BWYYNO9Y=m3|~~;W?!=`yDdI{9rOe__~PSW=Sn19kzpQ{C9vT! zf3-UMCw+_8cqaR@mFT=GqZ0Z=dIu*1yC#b`2MUDC zLY9oP22f*=#fU3H;xGXT6P|PVq)4CW{-Xmu=_3b*Ug_(8iwWqsnrCp6p}CN>T!GXz z93Ck+1=DdgGe^1PxQq#qc&PSdQegrd9&$!b7HQ7qVMxj$@gSE@(-K$CX!;0sgw1V& z9v)A>m>qN|bF$PurJ33Nw%x6{TK0&Q)AZ~BHSDZqQ!{4~n$-*DX-ak(8aqv_cg$UO zDeJIYO+4g_vy9|Zc8Nz^Ysh5a&y}9Z@It)#iZeZWdGuP#Y`D5*cO|~N8s5Dqq2|`bAPTGRj-DMA zI^}C`&h~$LeC~L4dq*YSaWmX;w`J3`b=kVrvU9#==dUfh&POJt$w7Fp($G1tb}mGl zZYMT ziPx9l0Qz5_6EX6lFayl=8t+#&UJ11w=jv z5$Z<%2|srJOr$YT?MF$amj_H2x_mHiF(@*QBmeV8;JXL@OHaYzckL7Kj_?6GOt6e= z-BR`Y0ow+ub0`_$fz;DnhJvAvxW*5AUFR@-n?`NcHP#9Um^Extow_JDrz^FxzY|V5 zkQ2+*QrWzBZg6Jc3v#u4Zt#I4TF2Q?J$?`VQ1JpnUI#^7(DcF>6onCl-;=n?*{!-uhzL{qx z`>N3$x1xLIqkF2+XJJE4TzcXB3s;WK3v;WR_g0(tR@KfM?!20=$Z3{@d%OrGn|LB; z={hv1f@L}>?PQJ#J0Mw0cEKEGB3a>6O7eQ)Q!}VzYdPw5QXaH5 z1|jg;A!`!asu;wl0Xpsov>~rhykIf$0r31|?Sk_RYZIE`PQxN2I_+XF!%|eQGYP|@ zgfYH@w%Epm6dwP3y}S8olg(dg3ay%YLxK=+pemJ0Q&1oTq@)~H3zln7Qa4_&89QpX zN~KC|Q6rGp6^Cslq?TJzIB*0msAnWZw4fRbiAo$0x0HkfoOsXPZIe(T=J3t?v**1x z@3X(n=QRTB?g!K7dPPEhMWwT(wy^mY2u)%Wn{!EpD{&RR#B-?gu22z6Vnr%R9O22U z#1^g+TcqNekUm?I6RQMPd7D+KDZ2VQhUk1OobYO~GUJ4lnJ(y=m(75_QMIWXYbUGT zg0nc~SfQ1^Fh$5TS(_JNYZ6LIoJ~snX~=;JHupKd#-#xzkxI5O1PYaH@fhw3NYX<| zP9&9PXl8+L$0>J43VZR4(-|@V0hUEahuiJqKA={29kvn%B9w$FKU!t{GC} zOwI7j3WZYC^UjjvLO`rmOtxfK&wFtObtA0#G?qh?Ez&R+LwW%)@J%nyI)MQiWuEEA zGNa)#^Q?9?Nlh{!c6akSK$EPHbBTE;fR*2AK@F^(&TVssN=E^|+L0gaonC@BnUzN# zlx*%3X%4P&R+nNGOPhzvSROpA3!xhz)|}u}yCz*`IkZ2Fh(*^4!X%FMfCpMB2Gm^$ zu;>GSmrfonpAIMs$}~J{dgT+oKV^nyd8TTaZh4+o{BqT&o(1z##^b3y(P{OBUn>X9 zYMYIYxs1IJCjDC6W5IY;S;h1$YS=Jj7YExHI=S0}Wdtt(TqgGi_ucBc*%cLrn@f$Q zsBf@2-I)HlZ}3Lnt)ZJkAHNw5@4q)(Tpunz$jUw129Yz`COk-H^{!YolF=}XSTl@D z)n0Zf$T`D!Z`pKlXbgkl1;;sXQrEyVoJEGW7Izzl>3P-A44tYMz+(cmEtbM%-=(qO zcrZa{s#P}@u@Z(O!Uh38BMj%74Itq4CV+mKka?uPWZ##=P%FP?=E~lFtOe} z*%BwCT=$jNBQ1YrI?}r9LVd2Tu0GQehoav8me{``Dw1~l(Cy&P$=eq`o!lazrpMq3 zkLT+9;MvYqWa>qjJu2FK!Z?YhOciY*(Pb)7q0LIM&0e%A4{a%r1$aZsV~MH>hebQ# zu&vT<@xa^+FgH``XiA%Ke39)51265~Nf{su`7)N*-Kv(|TpG=|7vjW5- z*`bBlhyw@Gk;ECjxNqBa{EK@}LPYR30B~Ob{W@ry%s`(9yuy)gb`VdBf;djF{hc?DUiWVFBj$dSS;T!5{< zD-WD_s!P|{Feq#UVIRUM!hVE)go6kPwGt*_?(7i4BZ?hH6#-4bJis*)soCrHs&-{I z$^w^GUu=neQNFi+upZQptiIb4M}Spz>&A2SvyJ^NaWthJget2qwZy>krerx`6w^-3I8r28wqzw%YWNkqwiG8$;t(>J6Ymt0;R|~w zTarmsZ4K0J0$4!`SOE-FAN^2Z7d47}75zwy0!`7N;F)S@99+~v0u=od;sQbQ)!9An z@hmBoQn)}4)a>l+%}&MrV9Jb0{Y!T%6V7PPn0k&w6rFLaNie2`|uutS{$J_;Z0oAQwyoIm9DXQk!moR`O+{ zR_TOm(G+pUkC5V`&`n^X88o>w+nkaXt~M(k!jajry6r}t#QIN!Rc~hJYQD^_)igy? z!-{tcG$_7H=%)XMVDSJwq68FfLR5m6MAk#1O=(iwm4Mm~^w1?Su|;XN>n%|4nC9Y} z{z+7f#5q%ZQP$7J1yeeaQx0QXz^1DxV@);vdLfzDak`jQHB;18O`mDart@#9N>@(C zZz+Wf`7W)frc8gTke?{voSaXoGeIcQ6TsY^(F%D}Ov#|iMWB|d4?Q)W2T0tw9{_L- zm0+5QgD;~vKVtG}VwSz-ruV^aP##H^BoF6+-EFO||dF(_XC1=M@ ztGVJ>p{V9l1w|cOX=QPGjF_)cm~>d%U7R+h=dyBcT#<)y2s$FxXcB-~^wH^$xBotI zA8;_m6w-=jy0NNH;`{@S1fiWevK?fvA>DS8xx{9?%qe^cokw#Vj_UOpgjj+2U-{{7 zMq#phP?^`sXwa|3F*=QSSP3`CaKgu-AMhEvwyZjXWuYWwh;J*tvIti4G_nK&%dV1G za=is{S$3D)+V6BSCiKfRGptc6V#z(hDO6^Hzft!@aqD5mlWApGqhxV<9%-8$`Xdbv zhLw2cCw6kSv-U>W;LLN-f5Ymvcm<|)9X@e`d@jbPc&>{xFn48-A}QXIC(Y5Cq;F-G zI(mGB(Hdn*Vwza_BiQ$*xUv9$BK*0_u4%MZ9F)B!lzFV7iD8N_ZO6u%?S9r1X!p(W z*j@5wI+=8%EDJm;32f%dqHgbgne7cOhL!z}Rp4Vilmno{UkcduAjDdb#o8RF{njx$ z-yCfLBISs-;6sZxFSBgjXtOyX+JX+NA4;?-O&j)Ti=*!~)=y)WY|oq$YgoHl0z7PT z^sypJft*l*dpf+v&LAK>^NRGI&4m`pn09Kvsc49d_gu2 zO3QUG-Bp^DO-i`bRNrGl8{T6=OcN_t?lG_v_~9>54*rjON-NV0TAE=`nbx0VlT)%+ z4(uUun`-a0Ww&GJXl8oK0SB)=Vql-UzJK^j{yAP7a*W~H`-dM+tIb5pv42E193jrN zvqpsI_HB;Yjhzx}ten*n5HS&@?V(xp1RLQETeM07ENX*2?qQwi_WZBAifm4>V7)!o zjvq$^tTEmtHya1#yEp6Ky}h&jq0PFzlGmMen-gZeeVy5~cY<~J8hhuK>%RZX-bwFf zjeT<(O`{m~>pTQ(n zWhFV0o`Po^yl5?ll11|3^j2tfOQQ z!O8@T(N$t2P^}3Aq%k|xLfh+x)4g@$!5zzXHgDfFMBi#0HYM}qQJziDsLA>>9+QWq zDS^S(82IQI5T|<^1A)nn9+MOX9SBU01WZnFOb)pCW=!t>5FFFwO$|?CISp@nOdfxj zgBX%*#2|^d>cVLM0l^rUNu`(}ldXdcsMXg3yx?yjeLf>(F8fZ>qZ4 zyfPf|%4f|^T^#``0Cj!XRi+~1-CD{?gm=so>~e#`jvp1 zcH9fKUcGqbVnrIhe6bdK%m_UO=tBQu(1`85b>4^#L$w0L*su{Au1VzgS4Od_HZ(xT zq*bdn&?Tyw3f`uI76-v&b%nMDKtTB&sp$(}XwFylZJj-MFVb=C+`H%IrxrI=BTv^N zJw~MGR(mxvRPhYWUNk&IcS4(Lp_mbhRisgX)zA(=Znpla{!kD z;Ypj5p*9m}|7e%p%uv#Q|A<8<{m1KG*kTp}#@`FKT^oFNaQ=mb@oG3;3wIgeu0^#P z?yq?IXQvEL|J`j}i>d0izKS%y6nea}t*;u|4frQrl{0TtkIRHSaIA7%t`3X?UV+Ae zaU(QdlgRH=MzN|kG^jGGT6N$y!)~l>|0o>7{2R+3z6ZdJH73j4Rzf=w43&NiJz*PoG711CPw>Mj%%6 zcN_lhs=ucq_F(eh!o;ix3?>HxG3x#w0FW$i#!im{q5dh57-b2}%ztqqL$s~0v#1ogrqbu5~yj! zN!YHfAtr|pJ%M6?XlDS-qC3&p&vWnR7LVLIRE_SdMV~jK&sU=dYSE)c^ytUpQuJgs z@Tw6#Ij7t{^~z6ASE46p4_5-O-tmX7?z^(@XRpp5e(!A6A6rli|5Fw5segYdpx|qq zMl!>{_8fRr{EY}8zSWf2;fr@eler~n9oOB~K?Fx=H&!p;v<^qFO(Jen8tw%d>oE0@ zP|c>tG4Tv{6I~%I3!h~FD`?yh>bH<2+{?0RGD+{5D*UJ6`$YCx0!|PG62$1SBZpr; zZHhz(y}iLD07iO7HAQl-q(_-4z~EbPN=K69@6GnKj^6V)ie5!I< zHo|fRHQ(`XzTAIx=*m!~?a9SwjJAEX$RQ(gs2Vv^^&g#;mfeV#ezNt4TWegq!L`pv z7TOn|G#>9W21hGgdzE`}nGbO(ZoYpRk#gbCm$bYuApR6*wJmSwxZZhT89}ii+$Y7d T#B;mmQ_Bd7h19=DK^^@MANFfj From 8817b2ae6aa34c0acf424b09f57c2619c98528d5 Mon Sep 17 00:00:00 2001 From: Ethan Davidson <31261035+EthanThatOneKid@users.noreply.github.com> Date: Sun, 5 Jul 2026 20:03:18 -0700 Subject: [PATCH 03/10] Fix frontmatter parsing for all format markers, make @id/@type optional, fix test harness - Rewrite _extract_frontmatter to support all LMD format markers: ---, ---yaml, ---json, ---toml, +++, = yaml =, = json =, = toml = - Auto-detect JSON vs YAML for plain --- delimiter - Add extract() public API that returns raw attrs + frontMatter + body - Remove mandatory @id and @type guards (missing is valid per spec) - Strip UTF-8 BOM before parsing - Catch JSON/YAML/TOML parse errors and wrap as LMD_INVALID_FRONTMATTER - Fix test harness: properly test extract() against conformance suite --- src/linked_markdown/__init__.py | 3 +- src/linked_markdown/parser.py | 114 ++++++++++++++++++++++++-------- tests/test_conformance.py | 29 ++++---- 3 files changed, 102 insertions(+), 44 deletions(-) diff --git a/src/linked_markdown/__init__.py b/src/linked_markdown/__init__.py index 6b33ff4..4c994f9 100644 --- a/src/linked_markdown/__init__.py +++ b/src/linked_markdown/__init__.py @@ -1,4 +1,4 @@ -from .parser import LmdError, parse +from .parser import LmdError, extract, parse from .rdf import to_graph, to_ntriples, to_triples from .types import LmpDocument, LmpLink @@ -6,6 +6,7 @@ "LmdError", "LmpDocument", "LmpLink", + "extract", "parse", "to_graph", "to_ntriples", diff --git a/src/linked_markdown/parser.py b/src/linked_markdown/parser.py index 1a2f821..9396553 100644 --- a/src/linked_markdown/parser.py +++ b/src/linked_markdown/parser.py @@ -1,3 +1,6 @@ +import json +import re +import tomllib from pathlib import Path from typing import Any @@ -6,6 +9,8 @@ from .links import extract_links from .types import LmpDocument +VALID_MARKERS = frozenset({"yaml", "json", "toml"}) + class LmdError(Exception): def __init__(self, code: str, message: str) -> None: @@ -15,47 +20,100 @@ def __init__(self, code: str, message: str) -> None: def parse(content: str, base_iri: str | None = None, file_path: Path | None = None) -> LmpDocument: del base_iri, file_path - normalized = content.replace("\r\n", "\n") - frontmatter, body = _extract_frontmatter(normalized) - context = _normalize_context(frontmatter.get("@context")) - doc_id = _read_string(frontmatter.get("@id") or frontmatter.get("id")) - - if not doc_id: - raise LmdError("LMD_MISSING_ID", "Expected id or @id in frontmatter.") - - raw_types = frontmatter.get("@type", frontmatter.get("type")) - if raw_types is None: - raise LmdError("LMD_MISSING_TYPE", "Expected @type or type in frontmatter.") + result = extract(content) + attrs = result["attrs"] + body = result["body"] - types = [_expand_curie(item, context) for item in _normalize_string_array(raw_types)] + context = _normalize_context(attrs.get("@context")) + doc_id = _read_string(attrs.get("@id") or attrs.get("id")) + raw_types = attrs.get("@type", attrs.get("type")) + types = [_expand_curie(item, context) for item in _normalize_string_array(raw_types)] if raw_types is not None else [] return LmpDocument( id=doc_id, types=types, context=context, - frontmatter=frontmatter, + frontmatter=attrs, body=body, links=extract_links(body), ) -def _extract_frontmatter(content: str) -> tuple[dict[str, Any], str]: - if not content.startswith("---\n"): - raise LmdError("LMD_MISSING_FRONTMATTER", "Expected frontmatter delimited by --- at the start of the document.") - - end = content.find("\n---", 4) - if end == -1: - raise LmdError("LMD_MISSING_FRONTMATTER", "Expected closing frontmatter delimiter.") - - raw_frontmatter = content[4:end] - body = content[end + 4 :] - body = body.lstrip("\n") - - parsed = yaml.safe_load(raw_frontmatter) or {} - if not isinstance(parsed, dict): +def extract(content: str) -> dict[str, Any]: + content = content.replace("\ufeff", "").replace("\r\n", "\n") + + _check_unknown_markers(content) + + has_opener = bool(re.match(r"^(---|---\w+\n|\+\+\+\n|= \w+ =\n)", content)) + if not has_opener: + raise LmdError("LMD_NO_FRONTMATTER", "Expected frontmatter at the start of the document.") + + patterns: list[tuple[str, str, str]] = [ + (r"^---yaml\n", r"\n---", "yaml"), + (r"^---json\n", r"\n---", "json"), + (r"^---toml\n", r"\n---", "toml"), + (r"^\+\+\+\n", r"\n\+\+\+", "toml"), + (r"^= yaml =\n", r"\n= yaml =", "yaml"), + (r"^= json =\n", r"\n= json =", "json"), + (r"^= toml =\n", r"\n= toml =", "toml"), + ] + + for opener_pat, closer_pat, fmt in patterns: + opener_match = re.match(opener_pat, content) + if opener_match: + opener_len = len(opener_match.group(0)) + remaining = content[opener_len:] + end_match = re.search(closer_pat, remaining) + if end_match is None: + raise LmdError("LMD_INVALID_FRONTMATTER", "Expected closing frontmatter delimiter.") + raw_fm = remaining[:end_match.start()] + body = remaining[end_match.end():].lstrip("\n") + return _build_extract_result(raw_fm, body, fmt) + + if content.startswith("---\n"): + end = content.find("\n---", 3) + if end == -1: + raise LmdError("LMD_INVALID_FRONTMATTER", "Expected closing frontmatter delimiter.") + raw_fm = content[4:end] + body = content[end + 4:].lstrip("\n") + fmt = "json" if raw_fm.strip().startswith(("{", "[")) else "yaml" + return _build_extract_result(raw_fm, body, fmt) + + raise LmdError("LMD_INVALID_FRONTMATTER", "Unrecognized frontmatter delimiter pattern.") + + +def _check_unknown_markers(content: str) -> None: + marker_match = re.match(r"^---(\w+)", content) + if marker_match: + marker = marker_match.group(1).lower() + if marker and marker not in VALID_MARKERS: + raise LmdError("LMD_INVALID_FRONTMATTER", f"Unknown front matter marker: ---{marker}") + + equals_match = re.match(r"^= (\w+) =", content) + if equals_match: + marker = equals_match.group(1).lower() + if marker not in VALID_MARKERS: + raise LmdError("LMD_INVALID_FRONTMATTER", f"Unknown front matter marker: = {marker} =") + + +def _build_extract_result(raw_fm: str, body: str, fmt: str) -> dict[str, Any]: + try: + if fmt == "json": + attrs = json.loads(raw_fm) if raw_fm.strip() else {} + elif fmt == "toml": + attrs = tomllib.loads(raw_fm) if raw_fm.strip() else {} + else: + attrs = yaml.safe_load(raw_fm) if raw_fm.strip() else {} + except (json.JSONDecodeError, tomllib.TOMLDecodeError, yaml.YAMLError) as e: + raise LmdError("LMD_INVALID_FRONTMATTER", f"Invalid {fmt} frontmatter: {e}") from e + + if attrs is None: + attrs = {} + if not isinstance(attrs, dict): raise LmdError("LMD_INVALID_FRONTMATTER", "Frontmatter must parse to an object.") - return parsed, body + front_matter = raw_fm + "\n" if raw_fm else "" + return {"attrs": attrs, "frontMatter": front_matter, "body": body} def _normalize_context(value: Any) -> dict[str, str]: diff --git a/tests/test_conformance.py b/tests/test_conformance.py index 99f4e94..a5b64dc 100644 --- a/tests/test_conformance.py +++ b/tests/test_conformance.py @@ -3,7 +3,7 @@ import pytest -from linked_markdown import LmdError, parse, to_ntriples +from linked_markdown import LmdError, extract CONFORMANCE_ROOT = Path(__file__).parent.parent / "test" / "linked-markdown-spec" / "conformance" MANIFEST = json.loads((CONFORMANCE_ROOT / "manifest.json").read_text()) @@ -13,25 +13,24 @@ def test_conformance(case: dict) -> None: input_text = _read_fixture(case["input"]) - if "expectError" in case: + expect = case.get("expect", {}) + + if "error" in expect: with pytest.raises(LmdError) as error: - parse(input_text) - assert error.value.code == case["expectError"]["code"] + extract(input_text) + assert error.value.code == expect["error"]["code"] return - doc = parse(input_text) + result = extract(input_text) - if parsed_path := case.get("expect", {}).get("parsed"): - assert doc.conformance_dict() == json.loads(_read_fixture(parsed_path)) + if parsed := expect.get("parsed"): + expected = json.loads(_read_fixture(parsed)) + assert result["attrs"] == expected, f"attrs mismatch for {case['id']}" - if rdf_path := case.get("expect", {}).get("rdf"): - assert _normalize_triples(to_ntriples(doc)) == _normalize_triples(_read_fixture(rdf_path)) + if extracted := expect.get("extracted"): + expected = json.loads(_read_fixture(extracted)) + assert result == expected, f"extracted mismatch for {case['id']}" def _read_fixture(path: str) -> str: - return (CONFORMANCE_ROOT / path).read_text() - - -def _normalize_triples(value: str) -> str: - lines = [line for line in value.replace("\r\n", "\n").strip().split("\n") if line] - return "\n".join(sorted(lines)) + "\n" + return (CONFORMANCE_ROOT / path).read_text(encoding="utf-8") From 70bc690ccec2d08827e0d1e64f699220126df573 Mon Sep 17 00:00:00 2001 From: Ethan Davidson <31261035+EthanThatOneKid@users.noreply.github.com> Date: Sun, 5 Jul 2026 20:11:52 -0700 Subject: [PATCH 04/10] Add workflow_dispatch trigger for manual CI runs, fix LF line endings --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2648e92..31a9e15 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,10 +1,11 @@ -name: CI +name: CI on: push: branches: [main] pull_request: branches: [main] + workflow_dispatch: jobs: test: From bc94f4a8b691b8fc7e4c9ffe159cb22807741cef Mon Sep 17 00:00:00 2001 From: Ethan Davidson <31261035+EthanThatOneKid@users.noreply.github.com> Date: Sun, 5 Jul 2026 20:14:24 -0700 Subject: [PATCH 05/10] Update conformance submodule to latest spec with expanded test suite --- test/linked-markdown-spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/linked-markdown-spec b/test/linked-markdown-spec index 181b8c8..e4350ee 160000 --- a/test/linked-markdown-spec +++ b/test/linked-markdown-spec @@ -1 +1 @@ -Subproject commit 181b8c868128896f766b36af544e9a5c3aa081c3 +Subproject commit e4350eef21b15ad6e3a1d4282083a9d4f6f73b04 From 1d9c0a664dd75860124f6e8cce3b9d6ee5399d24 Mon Sep 17 00:00:00 2001 From: Ethan Davidson <31261035+EthanThatOneKid@users.noreply.github.com> Date: Sun, 5 Jul 2026 20:25:52 -0700 Subject: [PATCH 06/10] Simplify Python impl to match TS API surface: only extract() + LinkedMarkdownError - Remove rdf.py, links.py, types.py (over-engineering) - Remove parse() and all helper functions - Rename LmdError to LinkedMarkdownError (matches TS naming) - Strip pydantic and rdflib dependencies from pyproject.toml - Update README API example --- README.md | 6 +- pyproject.toml | 4 +- src/linked_markdown/__init__.py | 12 +-- src/linked_markdown/links.py | 42 -------- src/linked_markdown/parser.py | 72 ++----------- src/linked_markdown/rdf.py | 43 -------- src/linked_markdown/types.py | 41 -------- tests/test_conformance.py | 4 +- uv.lock | 176 +------------------------------- 9 files changed, 19 insertions(+), 381 deletions(-) delete mode 100644 src/linked_markdown/links.py delete mode 100644 src/linked_markdown/rdf.py delete mode 100644 src/linked_markdown/types.py diff --git a/README.md b/README.md index 59c9f6c..8b294b0 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,10 @@ Python implementation of Linked Markdown, packaged for PyPI. ## API ```python -from linked_markdown import parse, to_graph +from linked_markdown import extract -doc = parse(markdown) -graph = to_graph(doc) +result = extract(markdown) +# => {"attrs": {...}, "frontMatter": "...", "body": "..."} ``` ## Development diff --git a/pyproject.toml b/pyproject.toml index 30cd17e..9535675 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,9 +9,7 @@ authors = [ { name = "Wazoo Technology" } ] dependencies = [ - "pydantic>=2.11.0", - "pyyaml>=6.0.0", - "rdflib>=7.1.0" + "pyyaml>=6.0.0" ] [dependency-groups] diff --git a/src/linked_markdown/__init__.py b/src/linked_markdown/__init__.py index 4c994f9..e10d91d 100644 --- a/src/linked_markdown/__init__.py +++ b/src/linked_markdown/__init__.py @@ -1,14 +1,6 @@ -from .parser import LmdError, extract, parse -from .rdf import to_graph, to_ntriples, to_triples -from .types import LmpDocument, LmpLink +from .parser import LinkedMarkdownError, extract __all__ = [ - "LmdError", - "LmpDocument", - "LmpLink", + "LinkedMarkdownError", "extract", - "parse", - "to_graph", - "to_ntriples", - "to_triples", ] diff --git a/src/linked_markdown/links.py b/src/linked_markdown/links.py deleted file mode 100644 index e1701fc..0000000 --- a/src/linked_markdown/links.py +++ /dev/null @@ -1,42 +0,0 @@ -import re - -from .types import LmpLink - -_EXTERNAL_TARGET = re.compile(r"^[a-z][a-z0-9+.-]*:", re.IGNORECASE) -_CODE_SPAN = re.compile(r"`[^`]*`") -_MARKDOWN_LINK = re.compile(r"\[([^\]\n]+)\]\(([^)\n]+)\)") -_WIKILINK = re.compile(r"\[\[([^\]\n]+)\]\]") - - -def extract_links(body: str) -> list[LmpLink]: - masked = _CODE_SPAN.sub(lambda match: " " * len(match.group(0)), body) - links: list[LmpLink] = [] - - for match in _MARKDOWN_LINK.finditer(masked): - raw = match.group(0) - text = match.group(1) - target = match.group(2) - links.append( - LmpLink( - link_type="markdown", - target=target, - text=text, - span=(match.start(), match.start() + len(raw)), - is_external=bool(_EXTERNAL_TARGET.match(target)), - ) - ) - - for match in _WIKILINK.finditer(masked): - raw = match.group(0) - text = match.group(1) - links.append( - LmpLink( - link_type="wikilink", - target=text, - text=text, - span=(match.start(), match.start() + len(raw)), - is_external=bool(_EXTERNAL_TARGET.match(text)), - ) - ) - - return sorted(links, key=lambda link: link.span[0]) diff --git a/src/linked_markdown/parser.py b/src/linked_markdown/parser.py index 9396553..c60c741 100644 --- a/src/linked_markdown/parser.py +++ b/src/linked_markdown/parser.py @@ -1,52 +1,27 @@ import json import re import tomllib -from pathlib import Path from typing import Any import yaml -from .links import extract_links -from .types import LmpDocument - VALID_MARKERS = frozenset({"yaml", "json", "toml"}) -class LmdError(Exception): +class LinkedMarkdownError(Exception): def __init__(self, code: str, message: str) -> None: super().__init__(message) self.code = code -def parse(content: str, base_iri: str | None = None, file_path: Path | None = None) -> LmpDocument: - del base_iri, file_path - result = extract(content) - attrs = result["attrs"] - body = result["body"] - - context = _normalize_context(attrs.get("@context")) - doc_id = _read_string(attrs.get("@id") or attrs.get("id")) - raw_types = attrs.get("@type", attrs.get("type")) - types = [_expand_curie(item, context) for item in _normalize_string_array(raw_types)] if raw_types is not None else [] - - return LmpDocument( - id=doc_id, - types=types, - context=context, - frontmatter=attrs, - body=body, - links=extract_links(body), - ) - - def extract(content: str) -> dict[str, Any]: content = content.replace("\ufeff", "").replace("\r\n", "\n") _check_unknown_markers(content) - has_opener = bool(re.match(r"^(---|---\w+\n|\+\+\+\n|= \w+ =\n)", content)) + has_opener = bool(re.match(r"^(---|\+\+\+(?:\n|$)|= \w+ =(?:\n|$))", content)) if not has_opener: - raise LmdError("LMD_NO_FRONTMATTER", "Expected frontmatter at the start of the document.") + raise LinkedMarkdownError("LMD_NO_FRONTMATTER", "Expected frontmatter at the start of the document.") patterns: list[tuple[str, str, str]] = [ (r"^---yaml\n", r"\n---", "yaml"), @@ -65,7 +40,7 @@ def extract(content: str) -> dict[str, Any]: remaining = content[opener_len:] end_match = re.search(closer_pat, remaining) if end_match is None: - raise LmdError("LMD_INVALID_FRONTMATTER", "Expected closing frontmatter delimiter.") + raise LinkedMarkdownError("LMD_INVALID_FRONTMATTER", "Expected closing frontmatter delimiter.") raw_fm = remaining[:end_match.start()] body = remaining[end_match.end():].lstrip("\n") return _build_extract_result(raw_fm, body, fmt) @@ -73,13 +48,13 @@ def extract(content: str) -> dict[str, Any]: if content.startswith("---\n"): end = content.find("\n---", 3) if end == -1: - raise LmdError("LMD_INVALID_FRONTMATTER", "Expected closing frontmatter delimiter.") + raise LinkedMarkdownError("LMD_INVALID_FRONTMATTER", "Expected closing frontmatter delimiter.") raw_fm = content[4:end] body = content[end + 4:].lstrip("\n") fmt = "json" if raw_fm.strip().startswith(("{", "[")) else "yaml" return _build_extract_result(raw_fm, body, fmt) - raise LmdError("LMD_INVALID_FRONTMATTER", "Unrecognized frontmatter delimiter pattern.") + raise LinkedMarkdownError("LMD_INVALID_FRONTMATTER", "Unrecognized frontmatter delimiter pattern.") def _check_unknown_markers(content: str) -> None: @@ -87,13 +62,13 @@ def _check_unknown_markers(content: str) -> None: if marker_match: marker = marker_match.group(1).lower() if marker and marker not in VALID_MARKERS: - raise LmdError("LMD_INVALID_FRONTMATTER", f"Unknown front matter marker: ---{marker}") + raise LinkedMarkdownError("LMD_INVALID_FRONTMATTER", f"Unknown front matter marker: ---{marker}") equals_match = re.match(r"^= (\w+) =", content) if equals_match: marker = equals_match.group(1).lower() if marker not in VALID_MARKERS: - raise LmdError("LMD_INVALID_FRONTMATTER", f"Unknown front matter marker: = {marker} =") + raise LinkedMarkdownError("LMD_INVALID_FRONTMATTER", f"Unknown front matter marker: = {marker} =") def _build_extract_result(raw_fm: str, body: str, fmt: str) -> dict[str, Any]: @@ -105,39 +80,12 @@ def _build_extract_result(raw_fm: str, body: str, fmt: str) -> dict[str, Any]: else: attrs = yaml.safe_load(raw_fm) if raw_fm.strip() else {} except (json.JSONDecodeError, tomllib.TOMLDecodeError, yaml.YAMLError) as e: - raise LmdError("LMD_INVALID_FRONTMATTER", f"Invalid {fmt} frontmatter: {e}") from e + raise LinkedMarkdownError("LMD_INVALID_FRONTMATTER", f"Invalid {fmt} frontmatter: {e}") from e if attrs is None: attrs = {} if not isinstance(attrs, dict): - raise LmdError("LMD_INVALID_FRONTMATTER", "Frontmatter must parse to an object.") + raise LinkedMarkdownError("LMD_INVALID_FRONTMATTER", "Frontmatter must parse to an object.") front_matter = raw_fm + "\n" if raw_fm else "" return {"attrs": attrs, "frontMatter": front_matter, "body": body} - - -def _normalize_context(value: Any) -> dict[str, str]: - if not isinstance(value, dict): - return {} - return {str(key): prefix for key, prefix in value.items() if isinstance(prefix, str)} - - -def _normalize_string_array(value: Any) -> list[str]: - if isinstance(value, str): - return [value] - if isinstance(value, list) and all(isinstance(item, str) for item in value): - return value - raise LmdError("LMD_INVALID_TYPE", "Expected @type or type to be a string or string array.") - - -def _read_string(value: Any) -> str | None: - return value if isinstance(value, str) and value else None - - -def _expand_curie(value: str, context: dict[str, str]) -> str: - if "://" in value: - return value - prefix, separator, suffix = value.partition(":") - if not separator: - return value - return f"{context[prefix]}{suffix}" if prefix in context else value diff --git a/src/linked_markdown/rdf.py b/src/linked_markdown/rdf.py deleted file mode 100644 index 42f0f13..0000000 --- a/src/linked_markdown/rdf.py +++ /dev/null @@ -1,43 +0,0 @@ -from collections.abc import Generator -from typing import Any - -from rdflib import Graph, Literal, URIRef -from rdflib.namespace import RDF - -from .parser import _expand_curie -from .types import LmpDocument - -ARTICLE_BODY = URIRef("https://schema.org/articleBody") - - -def to_triples(doc: LmpDocument) -> Generator[tuple[URIRef, URIRef, Any], None, None]: - if not doc.id: - return - - subject = URIRef(doc.id) - - for doc_type in doc.types: - yield subject, RDF.type, URIRef(doc_type) - - if doc.body: - yield subject, ARTICLE_BODY, Literal(doc.body) - - for key, value in doc.frontmatter.items(): - if key in {"id", "@id", "type", "@type", "@context"}: - continue - if not isinstance(value, str): - continue - yield subject, URIRef(_expand_curie(key, doc.context)), Literal(value) - - -def to_graph(doc: LmpDocument, graph: Graph | None = None) -> Graph: - target = graph or Graph() - for triple in to_triples(doc): - target.add(triple) - return target - - -def to_ntriples(doc: LmpDocument) -> str: - graph = to_graph(doc) - lines = graph.serialize(format="nt").splitlines() - return "\n".join(sorted(line for line in lines if line)) + "\n" diff --git a/src/linked_markdown/types.py b/src/linked_markdown/types.py deleted file mode 100644 index e2b76d7..0000000 --- a/src/linked_markdown/types.py +++ /dev/null @@ -1,41 +0,0 @@ -from typing import Any, Literal - -from pydantic import BaseModel, ConfigDict - - -class LmpLink(BaseModel): - model_config = ConfigDict(populate_by_name=True) - - link_type: Literal["wikilink", "markdown"] - target: str - text: str - span: tuple[int, int] - is_external: bool - - def conformance_dict(self) -> dict[str, Any]: - return { - "linkType": self.link_type, - "target": self.target, - "text": self.text, - "span": list(self.span), - "isExternal": self.is_external, - } - - -class LmpDocument(BaseModel): - id: str | None - types: list[str] - context: dict[str, str] - frontmatter: dict[str, Any] - body: str - links: list[LmpLink] - - def conformance_dict(self) -> dict[str, Any]: - return { - "id": self.id, - "types": self.types, - "context": self.context, - "frontmatter": self.frontmatter, - "body": self.body, - "links": [link.conformance_dict() for link in self.links], - } diff --git a/tests/test_conformance.py b/tests/test_conformance.py index a5b64dc..907f3d7 100644 --- a/tests/test_conformance.py +++ b/tests/test_conformance.py @@ -3,7 +3,7 @@ import pytest -from linked_markdown import LmdError, extract +from linked_markdown import LinkedMarkdownError, extract CONFORMANCE_ROOT = Path(__file__).parent.parent / "test" / "linked-markdown-spec" / "conformance" MANIFEST = json.loads((CONFORMANCE_ROOT / "manifest.json").read_text()) @@ -16,7 +16,7 @@ def test_conformance(case: dict) -> None: expect = case.get("expect", {}) if "error" in expect: - with pytest.raises(LmdError) as error: + with pytest.raises(LinkedMarkdownError) as error: extract(input_text) assert error.value.code == expect["error"]["code"] return diff --git a/uv.lock b/uv.lock index 00c2df4..4c4465e 100644 --- a/uv.lock +++ b/uv.lock @@ -2,15 +2,6 @@ version = 1 revision = 3 requires-python = ">=3.11" -[[package]] -name = "annotated-types" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -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" } -wheels = [ - { 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" }, -] - [[package]] name = "colorama" version = "0.4.6" @@ -34,9 +25,7 @@ name = "linked-markdown" version = "0.1.0" source = { editable = "." } dependencies = [ - { name = "pydantic" }, { name = "pyyaml" }, - { name = "rdflib" }, ] [package.dev-dependencies] @@ -45,11 +34,7 @@ dev = [ ] [package.metadata] -requires-dist = [ - { name = "pydantic", specifier = ">=2.11.0" }, - { name = "pyyaml", specifier = ">=6.0.0" }, - { name = "rdflib", specifier = ">=7.1.0" }, -] +requires-dist = [{ name = "pyyaml", specifier = ">=6.0.0" }] [package.metadata.requires-dev] dev = [{ name = "pytest", specifier = ">=8.4.0" }] @@ -72,123 +57,6 @@ wheels = [ { 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" }, ] -[[package]] -name = "pydantic" -version = "2.13.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, -] - -[[package]] -name = "pydantic-core" -version = "2.46.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, - { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, - { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, - { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, - { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, - { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, - { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, - { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, - { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, - { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, - { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, - { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, - { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, - { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, - { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, - { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, - { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, - { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, - { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, - { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, - { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, - { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, - { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, - { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, - { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, - { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, - { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, - { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, - { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, - { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, - { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, - { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, - { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, - { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, - { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, - { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, - { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, - { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, - { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, - { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, - { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, - { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, - { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, - { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, - { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, - { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, - { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, - { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, - { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, - { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, - { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, - { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, - { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, - { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, - { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, - { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, - { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, - { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, - { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, - { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, - { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, - { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, - { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, - { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, - { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, - { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, - { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, - { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, - { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, - { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, - { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, - { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, - { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, - { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, - { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, - { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, - { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, - { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, - { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, - { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, - { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, - { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, - { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, - { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, - { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, - { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, - { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, - { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, -] - [[package]] name = "pygments" version = "2.20.0" @@ -198,15 +66,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] -[[package]] -name = "pyparsing" -version = "3.3.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, -] - [[package]] name = "pytest" version = "9.1.1" @@ -277,36 +136,3 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] - -[[package]] -name = "rdflib" -version = "7.6.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyparsing" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/98/f5/18bb77b7af9526add0c727a3b2048959847dc5fb030913e2918bf384fec3/rdflib-7.6.0.tar.gz", hash = "sha256:6c831288d5e4a5a7ece85d0ccde9877d512a3d0f02d7c06455d00d6d0ea379df", size = 4943826, upload-time = "2026-02-13T07:15:55.938Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/10/c2/6604a71269e0c1bd75656d5a001432d16f2cc5b8c057140ec797155c295e/rdflib-7.6.0-py3-none-any.whl", hash = "sha256:30c0a3ebf4c0e09215f066be7246794b6492e054e782d7ac2a34c9f70a15e0dd", size = 615416, upload-time = "2026-02-13T07:15:46.487Z" }, -] - -[[package]] -name = "typing-extensions" -version = "4.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, -] - -[[package]] -name = "typing-inspection" -version = "0.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -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" } -wheels = [ - { 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" }, -] From 607fec711bc91bde9da75b7eb4b90bf7cd0a2a34 Mon Sep 17 00:00:00 2001 From: Ethan Davidson <31261035+EthanThatOneKid@users.noreply.github.com> Date: Sun, 5 Jul 2026 21:08:35 -0700 Subject: [PATCH 07/10] Add RDF integration tests: RDFLib (Python) and jsonld/RDF-JS (TS) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Python: rdflib dev dep, 5 tests proving extract() attrs load into Graph - TypeScript: 5 tests using jsonld.toRDF() proving attrs produce valid quads - No changes to distributed src/ — tests only --- pyproject.toml | 3 +- tests/test_rdf.py | 70 +++++++++++++++++++++++++++++++++++++++++++++++ uv.lock | 27 +++++++++++++++++- 3 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 tests/test_rdf.py diff --git a/pyproject.toml b/pyproject.toml index 9535675..91d3d38 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,8 @@ dependencies = [ [dependency-groups] dev = [ - "pytest>=8.4.0" + "pytest>=8.4.0", + "rdflib>=7.0.0" ] [build-system] diff --git a/tests/test_rdf.py b/tests/test_rdf.py new file mode 100644 index 0000000..6704dbe --- /dev/null +++ b/tests/test_rdf.py @@ -0,0 +1,70 @@ +import json +from pathlib import Path + +import pytest +import rdflib +from rdflib import RDF, Literal, URIRef + +from linked_markdown import extract + +CONFORMANCE_ROOT = Path(__file__).parent.parent / "test" / "linked-markdown-spec" / "conformance" +SCHEMA = rdflib.Namespace("https://schema.org/") + + +def _read_fixture(path: str) -> str: + return (CONFORMANCE_ROOT / path).read_text(encoding="utf-8") + + +def test_valid_yaml_marker_loads_into_rdflib(): + input_text = _read_fixture("cases/valid-yaml-marker/input.md") + result = extract(input_text) + + g = rdflib.Graph() + g.parse(data=json.dumps(result["attrs"]), format="json-ld") + + subject = URIRef("https://example.org/docs/fixture") + assert (subject, RDF.type, SCHEMA.Article) in g + assert (subject, SCHEMA.name, Literal("Delimiter Fixture")) in g + + +def test_missing_id_generates_blank_node(): + input_text = _read_fixture("cases/missing-id/input.md") + result = extract(input_text) + + g = rdflib.Graph() + g.parse(data=json.dumps(result["attrs"]), format="json-ld") + + subjects = {s for s, _, _ in g} + assert any(isinstance(s, rdflib.BNode) for s in subjects) + + +def test_missing_type_produces_no_rdf_type(): + input_text = _read_fixture("cases/missing-type/input.md") + result = extract(input_text) + + g = rdflib.Graph() + g.parse(data=json.dumps(result["attrs"]), format="json-ld") + + assert len(list(g.triples((None, RDF.type, None)))) == 0 + + +def test_bare_keywords_produces_no_triples_without_context(): + input_text = _read_fixture("cases/bare-keywords-preserved/input.md") + result = extract(input_text) + + g = rdflib.Graph() + g.parse(data=json.dumps(result["attrs"]), format="json-ld") + + assert len(g) == 0 + + +def test_canonical_type(): + input_text = _read_fixture("cases/canonical-type/input.md") + result = extract(input_text) + + g = rdflib.Graph() + g.parse(data=json.dumps(result["attrs"]), format="json-ld") + + subject = URIRef("https://example.org/docs/canonical-type") + assert (subject, RDF.type, SCHEMA.Note) in g + assert (subject, SCHEMA.name, Literal("Canonical Type")) in g diff --git a/uv.lock b/uv.lock index 4c4465e..6377bf8 100644 --- a/uv.lock +++ b/uv.lock @@ -31,13 +31,17 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "pytest" }, + { name = "rdflib" }, ] [package.metadata] requires-dist = [{ name = "pyyaml", specifier = ">=6.0.0" }] [package.metadata.requires-dev] -dev = [{ name = "pytest", specifier = ">=8.4.0" }] +dev = [ + { name = "pytest", specifier = ">=8.4.0" }, + { name = "rdflib", specifier = ">=7.0.0" }, +] [[package]] name = "packaging" @@ -66,6 +70,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + [[package]] name = "pytest" version = "9.1.1" @@ -136,3 +149,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] + +[[package]] +name = "rdflib" +version = "7.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyparsing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/f5/18bb77b7af9526add0c727a3b2048959847dc5fb030913e2918bf384fec3/rdflib-7.6.0.tar.gz", hash = "sha256:6c831288d5e4a5a7ece85d0ccde9877d512a3d0f02d7c06455d00d6d0ea379df", size = 4943826, upload-time = "2026-02-13T07:15:55.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/c2/6604a71269e0c1bd75656d5a001432d16f2cc5b8c057140ec797155c295e/rdflib-7.6.0-py3-none-any.whl", hash = "sha256:30c0a3ebf4c0e09215f066be7246794b6492e054e782d7ac2a34c9f70a15e0dd", size = 615416, upload-time = "2026-02-13T07:15:46.487Z" }, +] From a50aacd2648878aba0497cfd18206d528f7366af Mon Sep 17 00:00:00 2001 From: Ethan Davidson <31261035+EthanThatOneKid@users.noreply.github.com> Date: Sun, 5 Jul 2026 21:40:17 -0700 Subject: [PATCH 08/10] Add ruff linting+formatting to Python impl - Add ruff dev dependency and config (select E/F/I/N/W, line-length 100) - Remove unused pytest import in test_rdf.py - Auto-format parser.py - Add ruff check + ruff format --check to CI --- .github/workflows/ci.yml | 2 ++ pyproject.toml | 10 +++++++++- src/linked_markdown/parser.py | 34 ++++++++++++++++++++++++---------- tests/test_rdf.py | 1 - uv.lock | 27 +++++++++++++++++++++++++++ 5 files changed, 62 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 31a9e15..8680551 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,4 +17,6 @@ jobs: - uses: astral-sh/setup-uv@v6 + - run: uv run ruff check + - run: uv run ruff format --check - run: uv run pytest diff --git a/pyproject.toml b/pyproject.toml index 91d3d38..0f446f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,8 @@ dependencies = [ [dependency-groups] dev = [ "pytest>=8.4.0", - "rdflib>=7.0.0" + "rdflib>=7.0.0", + "ruff>=0.11.0" ] [build-system] @@ -25,5 +26,12 @@ build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] packages = ["src/linked_markdown"] +[tool.ruff] +line-length = 100 +target-version = "py311" + +[tool.ruff.lint] +select = ["E", "F", "I", "N", "W"] + [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/src/linked_markdown/parser.py b/src/linked_markdown/parser.py index c60c741..1aaa629 100644 --- a/src/linked_markdown/parser.py +++ b/src/linked_markdown/parser.py @@ -21,7 +21,9 @@ def extract(content: str) -> dict[str, Any]: has_opener = bool(re.match(r"^(---|\+\+\+(?:\n|$)|= \w+ =(?:\n|$))", content)) if not has_opener: - raise LinkedMarkdownError("LMD_NO_FRONTMATTER", "Expected frontmatter at the start of the document.") + raise LinkedMarkdownError( + "LMD_NO_FRONTMATTER", "Expected frontmatter at the start of the document." + ) patterns: list[tuple[str, str, str]] = [ (r"^---yaml\n", r"\n---", "yaml"), @@ -40,21 +42,27 @@ def extract(content: str) -> dict[str, Any]: remaining = content[opener_len:] end_match = re.search(closer_pat, remaining) if end_match is None: - raise LinkedMarkdownError("LMD_INVALID_FRONTMATTER", "Expected closing frontmatter delimiter.") - raw_fm = remaining[:end_match.start()] - body = remaining[end_match.end():].lstrip("\n") + raise LinkedMarkdownError( + "LMD_INVALID_FRONTMATTER", "Expected closing frontmatter delimiter." + ) + raw_fm = remaining[: end_match.start()] + body = remaining[end_match.end() :].lstrip("\n") return _build_extract_result(raw_fm, body, fmt) if content.startswith("---\n"): end = content.find("\n---", 3) if end == -1: - raise LinkedMarkdownError("LMD_INVALID_FRONTMATTER", "Expected closing frontmatter delimiter.") + raise LinkedMarkdownError( + "LMD_INVALID_FRONTMATTER", "Expected closing frontmatter delimiter." + ) raw_fm = content[4:end] - body = content[end + 4:].lstrip("\n") + body = content[end + 4 :].lstrip("\n") fmt = "json" if raw_fm.strip().startswith(("{", "[")) else "yaml" return _build_extract_result(raw_fm, body, fmt) - raise LinkedMarkdownError("LMD_INVALID_FRONTMATTER", "Unrecognized frontmatter delimiter pattern.") + raise LinkedMarkdownError( + "LMD_INVALID_FRONTMATTER", "Unrecognized frontmatter delimiter pattern." + ) def _check_unknown_markers(content: str) -> None: @@ -62,13 +70,17 @@ def _check_unknown_markers(content: str) -> None: if marker_match: marker = marker_match.group(1).lower() if marker and marker not in VALID_MARKERS: - raise LinkedMarkdownError("LMD_INVALID_FRONTMATTER", f"Unknown front matter marker: ---{marker}") + raise LinkedMarkdownError( + "LMD_INVALID_FRONTMATTER", f"Unknown front matter marker: ---{marker}" + ) equals_match = re.match(r"^= (\w+) =", content) if equals_match: marker = equals_match.group(1).lower() if marker not in VALID_MARKERS: - raise LinkedMarkdownError("LMD_INVALID_FRONTMATTER", f"Unknown front matter marker: = {marker} =") + raise LinkedMarkdownError( + "LMD_INVALID_FRONTMATTER", f"Unknown front matter marker: = {marker} =" + ) def _build_extract_result(raw_fm: str, body: str, fmt: str) -> dict[str, Any]: @@ -80,7 +92,9 @@ def _build_extract_result(raw_fm: str, body: str, fmt: str) -> dict[str, Any]: else: attrs = yaml.safe_load(raw_fm) if raw_fm.strip() else {} except (json.JSONDecodeError, tomllib.TOMLDecodeError, yaml.YAMLError) as e: - raise LinkedMarkdownError("LMD_INVALID_FRONTMATTER", f"Invalid {fmt} frontmatter: {e}") from e + raise LinkedMarkdownError( + "LMD_INVALID_FRONTMATTER", f"Invalid {fmt} frontmatter: {e}" + ) from e if attrs is None: attrs = {} diff --git a/tests/test_rdf.py b/tests/test_rdf.py index 6704dbe..d7b3f70 100644 --- a/tests/test_rdf.py +++ b/tests/test_rdf.py @@ -1,7 +1,6 @@ import json from pathlib import Path -import pytest import rdflib from rdflib import RDF, Literal, URIRef diff --git a/uv.lock b/uv.lock index 6377bf8..ee1abfc 100644 --- a/uv.lock +++ b/uv.lock @@ -32,6 +32,7 @@ dependencies = [ dev = [ { name = "pytest" }, { name = "rdflib" }, + { name = "ruff" }, ] [package.metadata] @@ -41,6 +42,7 @@ requires-dist = [{ name = "pyyaml", specifier = ">=6.0.0" }] dev = [ { name = "pytest", specifier = ">=8.4.0" }, { name = "rdflib", specifier = ">=7.0.0" }, + { name = "ruff", specifier = ">=0.11.0" }, ] [[package]] @@ -161,3 +163,28 @@ sdist = { url = "https://files.pythonhosted.org/packages/98/f5/18bb77b7af9526add wheels = [ { url = "https://files.pythonhosted.org/packages/10/c2/6604a71269e0c1bd75656d5a001432d16f2cc5b8c057140ec797155c295e/rdflib-7.6.0-py3-none-any.whl", hash = "sha256:30c0a3ebf4c0e09215f066be7246794b6492e054e782d7ac2a34c9f70a15e0dd", size = 615416, upload-time = "2026-02-13T07:15:46.487Z" }, ] + +[[package]] +name = "ruff" +version = "0.15.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/dc/35b341fc554ba02f217fc10da57d1a75168cfbcf75b0ef2202176d4c4f2d/ruff-0.15.20.tar.gz", hash = "sha256:1416eb04349192646b54de98f146c4f59afe37d0decfc02c3cbbf396f3a28566", size = 4755489, upload-time = "2026-06-25T17:20:37.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/d9/2d5014f0253ba541d2061d9fa7193f48e941c8b21bb88a7ff9bbe0bd0596/ruff-0.15.20-py3-none-linux_armv6l.whl", hash = "sha256:00e188c53e499c3c1637f73c91dcf2fb56d576cab76ce1be50a27c4e80e37078", size = 10839665, upload-time = "2026-06-25T17:19:44.702Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d3/ac1798ba64f670698867fcfc591d50e7e421bef137db564858f619a30fcf/ruff-0.15.20-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9ebd1fd9b9c95fc0bd7b2761aebec1f030013d2e193a2901b224af68fe47251b", size = 11208649, upload-time = "2026-06-25T17:19:48.787Z" }, + { url = "https://files.pythonhosted.org/packages/47/47/d3ac899991202095dfcf3d5176be4272642be3cf981a2f1a30f72a2afb95/ruff-0.15.20-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5b16cdd67ca108185cd36dce98c576350c03b1660a751de725fb049193a0632", size = 10622638, upload-time = "2026-06-25T17:19:51.354Z" }, + { url = "https://files.pythonhosted.org/packages/33/13/4e043fe30aa94d4ff5213a9881fc296d12960f5971b234a5263fdc225312/ruff-0.15.20-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3413bb3c3d2ca6a8208f1f4809cd2dca3c6de6d0b491c0e70847672bde6e6efd", size = 10984227, upload-time = "2026-06-25T17:19:54.044Z" }, + { url = "https://files.pythonhosted.org/packages/76/e6/92e7bf40388bc5800073b96564f56264f7e48bfd1a498f5ced6ae6d5a769/ruff-0.15.20-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd7ec42b3bb3da066488db093308a69c4ac5ee6d2af333a86ba6e2eb2e7dd44b", size = 10622882, upload-time = "2026-06-25T17:19:57.037Z" }, + { url = "https://files.pythonhosted.org/packages/13/7a/43460be3f24495a3aa46d4b16873e2c4941b3b5f0b00cf88c03b7b94b339/ruff-0.15.20-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1a36ad0eb77fba9aabfb69ede54de6f376d04ac18ebea022847046d340a8267", size = 11474808, upload-time = "2026-06-25T17:20:00.357Z" }, + { url = "https://files.pythonhosted.org/packages/27/a0/f37077884873221c6b33b4ab49eb18f9f88e54a16a25a5bca59bef46dd66/ruff-0.15.20-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b6df3b1e4610432f0386dba04d853b5f08cbbc903410c6fcc02f620f05aff53c", size = 12293094, upload-time = "2026-06-25T17:20:03.446Z" }, + { url = "https://files.pythonhosted.org/packages/a6/74/165545b60256a9704c21ac0ec4a0d07933b320812f9584836c9f4aca4292/ruff-0.15.20-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e89f198a1ea6ef0d727c1cf16088bc91a6cb0ab947dedc966715691647186eae", size = 11526176, upload-time = "2026-06-25T17:20:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/86/b1/a976a136d40ade83ce743578399865f57001003a409acadc0ecbb3051082/ruff-0.15.20-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309809086c2acb67624950a3c8133e80f32d0d3e27106c0cd60ff26657c9f24b", size = 11520767, upload-time = "2026-06-25T17:20:09.191Z" }, + { url = "https://files.pythonhosted.org/packages/19/0f/f032696cb01c9b54c0263fa393474d7758f1cdc021a01b04e3cbc2500999/ruff-0.15.20-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2d2374caa2f2c2f9e2b7da0a50802cfb8b79f55a9b5e49379f564544fbf56487", size = 11500132, upload-time = "2026-06-25T17:20:13.602Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f4/51b1a14bc69e8c224b15dab9cce8e99b425e0455d462caa2b3c9be2b6a8e/ruff-0.15.20-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a1ed17b65293e0c2f22fc387bc13198a5de94bf4429589b0ff6946b0feaf21a3", size = 10943828, upload-time = "2026-06-25T17:20:16.635Z" }, + { url = "https://files.pythonhosted.org/packages/71/4b/fe267640783cd02bf6c5cc290b1df1051be2ec294c678b5c15fe19e52343/ruff-0.15.20-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f701305e66b38ea6c91882490eb73459796808e4c6362a1b765255e0cdcd4053", size = 10645418, upload-time = "2026-06-25T17:20:19.4Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c0/a65aa4ec2f5e87a1df32dc3ec1fede434fe3dfd5cbcf3b503cafc676ab54/ruff-0.15.20-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b9c0c367ad8e5d0d5b5b8537864c469a0a0e55417aadfbeca41fa61333be9f4", size = 11211770, upload-time = "2026-06-25T17:20:22.033Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a4/0caa331d954ae2723d729d351c989cb4ca8b6077d5c6c2cb6de75e98c041/ruff-0.15.20-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:01cc00dd58f0df339d0e902219dd53990ea99996a0344e5d9cc8d45d5307e460", size = 11618698, upload-time = "2026-06-25T17:20:25.259Z" }, + { url = "https://files.pythonhosted.org/packages/10/9b/5f14927848d2fd4aa891fd88d883788c5a7baba561c7874732364045708c/ruff-0.15.20-py3-none-win32.whl", hash = "sha256:ed65ef510e43a137207e0f01cfcf998aeddb1aeeda5c9d35023e910284d7cf21", size = 10857322, upload-time = "2026-06-25T17:20:28.612Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/fe47c501f9dea92a26d788ff98bb5d92ed4cb4c88792c5c88af6b697dc8e/ruff-0.15.20-py3-none-win_amd64.whl", hash = "sha256:a525c81c70fb0380344dd1d8745d8cc1c890b7fc94a58d5a07bd8eb9557b8415", size = 11993274, upload-time = "2026-06-25T17:20:31.871Z" }, + { url = "https://files.pythonhosted.org/packages/d7/2b/9555445e1201d92b3195f45cdb153a0b68f24e0a4273f6e3d5ab46e212bb/ruff-0.15.20-py3-none-win_arm64.whl", hash = "sha256:2f5b2a6d614e8700388806a14996c40fab2c47b819ef57d790a34878858ed9ca", size = 11343498, upload-time = "2026-06-25T17:20:35.03Z" }, +] From 3a0ed51d4956356ffae196bb925e3a58c43368e3 Mon Sep 17 00:00:00 2001 From: Ethan Davidson <31261035+EthanThatOneKid@users.noreply.github.com> Date: Sun, 5 Jul 2026 21:43:04 -0700 Subject: [PATCH 09/10] Trigger CI From 0d4d8d86ea5f5c62d8af42417b2619387ba617b2 Mon Sep 17 00:00:00 2001 From: Ethan Davidson <31261035+EthanThatOneKid@users.noreply.github.com> Date: Sun, 5 Jul 2026 22:13:27 -0700 Subject: [PATCH 10/10] Fix ruff import sorting after conflict resolution --- src/linked_markdown/__init__.py | 2 +- src/linked_markdown/extract.py | 8 ++++---- tests/test_conformance.py | 2 +- tests/test_extract.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/linked_markdown/__init__.py b/src/linked_markdown/__init__.py index 90aa581..01e9b65 100644 --- a/src/linked_markdown/__init__.py +++ b/src/linked_markdown/__init__.py @@ -1,4 +1,4 @@ -from .errors import LinkedMarkdownError, LMD_INVALID_FRONTMATTER, LMD_NO_FRONTMATTER +from .errors import LMD_INVALID_FRONTMATTER, LMD_NO_FRONTMATTER, LinkedMarkdownError from .extract import ExtractResult, extract __all__ = [ diff --git a/src/linked_markdown/extract.py b/src/linked_markdown/extract.py index 3ff5178..59fd1b8 100644 --- a/src/linked_markdown/extract.py +++ b/src/linked_markdown/extract.py @@ -6,7 +6,7 @@ import yaml -from .errors import LinkedMarkdownError, LMD_INVALID_FRONTMATTER, LMD_NO_FRONTMATTER +from .errors import LMD_INVALID_FRONTMATTER, LMD_NO_FRONTMATTER, LinkedMarkdownError T = TypeVar("T") @@ -22,8 +22,8 @@ class ExtractResult(Generic[T]): _OPENER_RE = re.compile( r"^(?:" r"= yaml =|= json =|= toml =|" # equals delimiters - r"---yaml|---json|---toml|" # explicit markers - r"---|\+\+\+" # unmarked dash and plus + r"---yaml|---json|---toml|" # explicit markers + r"---|\+\+\+" # unmarked dash and plus r")", re.MULTILINE, ) @@ -82,7 +82,7 @@ def _split_front_matter( return format_, raw, body.lstrip("\n") return None - raw = rest[:closer_idx + 1].lstrip("\n") + raw = rest[: closer_idx + 1].lstrip("\n") body = rest[closer_idx + len(closer) + 1 :] return format_, raw, body.lstrip("\n") diff --git a/tests/test_conformance.py b/tests/test_conformance.py index 9cf7c0d..7704090 100644 --- a/tests/test_conformance.py +++ b/tests/test_conformance.py @@ -2,8 +2,8 @@ from pathlib import Path import pytest -from linked_markdown.errors import LinkedMarkdownError +from linked_markdown.errors import LinkedMarkdownError from linked_markdown.extract import extract diff --git a/tests/test_extract.py b/tests/test_extract.py index 052e7fd..04a8744 100644 --- a/tests/test_extract.py +++ b/tests/test_extract.py @@ -1,6 +1,6 @@ import pytest -from linked_markdown.errors import LinkedMarkdownError, LMD_INVALID_FRONTMATTER, LMD_NO_FRONTMATTER +from linked_markdown.errors import LMD_INVALID_FRONTMATTER, LMD_NO_FRONTMATTER, LinkedMarkdownError from linked_markdown.extract import extract