hadith-detector 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. hadith_detector/__init__.py +33 -0
  2. hadith_detector/_annotation.py +49 -0
  3. hadith_detector/_bundle.json +45 -0
  4. hadith_detector/_bundled.py +16 -0
  5. hadith_detector/_convenience.py +84 -0
  6. hadith_detector/_data/bundle.json +1 -0
  7. hadith_detector/_data/manifest.json +1 -0
  8. hadith_detector/_data/notices/LICENSES.md +17 -0
  9. hadith_detector/_data/notices/PROVENANCE.md +46 -0
  10. hadith_detector/_data/notices/README.md +866 -0
  11. hadith_detector/_data/notices/SOURCES.md +28 -0
  12. hadith_detector/_data/notices/UPSTREAM-UNLICENSE.txt +24 -0
  13. hadith_detector/_data/notices/collections.csv +14 -0
  14. hadith_detector/_data/notices/configs.csv +139 -0
  15. hadith_detector/_data/record-offsets.bin +0 -0
  16. hadith_detector/_data/records.jsonl +14940 -0
  17. hadith_detector/_data/tokens.bin +0 -0
  18. hadith_detector/_data/trigram-offsets.bin +0 -0
  19. hadith_detector/_data/trigram-postings.bin +0 -0
  20. hadith_detector/_data/trigrams.marisa +0 -0
  21. hadith_detector/_data/unigram-offsets.bin +0 -0
  22. hadith_detector/_data/unigram-postings.bin +0 -0
  23. hadith_detector/_data/vocabulary.marisa +0 -0
  24. hadith_detector/_fuzzy.py +272 -0
  25. hadith_detector/_marisa.py +102 -0
  26. hadith_detector/_resources.py +365 -0
  27. hadith_detector/_serialization.py +210 -0
  28. hadith_detector/_text.py +120 -0
  29. hadith_detector/config.py +31 -0
  30. hadith_detector/detector.py +296 -0
  31. hadith_detector/models.py +161 -0
  32. hadith_detector/py.typed +0 -0
  33. hadith_detector-0.1.0.dist-info/METADATA +170 -0
  34. hadith_detector-0.1.0.dist-info/RECORD +37 -0
  35. hadith_detector-0.1.0.dist-info/WHEEL +5 -0
  36. hadith_detector-0.1.0.dist-info/licenses/LICENSE +21 -0
  37. hadith_detector-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,33 @@
1
+ """Offline Arabic Hadith detection with bundled or explicitly supplied resources."""
2
+
3
+ from importlib.metadata import version
4
+
5
+ from ._convenience import annotate, clear_cache, detect, get_hadith
6
+ from .config import DetectorConfig
7
+ from .detector import HadithDetector
8
+ from .models import (
9
+ Detection,
10
+ HadithRecord,
11
+ HadithReference,
12
+ OmittedSourceToken,
13
+ SourceOccurrence,
14
+ WordAlignment,
15
+ )
16
+
17
+ __version__ = version("hadith-detector")
18
+
19
+ __all__ = [
20
+ "__version__",
21
+ "HadithDetector",
22
+ "DetectorConfig",
23
+ "detect",
24
+ "annotate",
25
+ "get_hadith",
26
+ "clear_cache",
27
+ "HadithRecord",
28
+ "SourceOccurrence",
29
+ "HadithReference",
30
+ "Detection",
31
+ "WordAlignment",
32
+ "OmittedSourceToken",
33
+ ]
@@ -0,0 +1,49 @@
1
+ """Lossless plain-text escaping and deterministic HTML fragments."""
2
+
3
+ from collections.abc import Sequence
4
+ from html import escape
5
+
6
+ from .models import Detection
7
+
8
+
9
+ def validate(text: str, format: str) -> None:
10
+ """Reject unsupported formats and characters HTML cannot preserve losslessly."""
11
+ if not isinstance(text, str):
12
+ raise TypeError("text must be a string")
13
+ if not isinstance(format, str):
14
+ raise TypeError("format must be a string")
15
+ if format != "html":
16
+ raise ValueError("Only format='html' is supported")
17
+ if any(char == "\x00" or 0xD800 <= ord(char) <= 0xDFFF for char in text):
18
+ raise ValueError("HTML annotation cannot preserve NUL or lone surrogate characters")
19
+
20
+
21
+ def _escape(text: str) -> str:
22
+ # Character references keep CR intact through HTML's newline preprocessing.
23
+ return escape(text, quote=True).replace("\r", "&#13;")
24
+
25
+
26
+ def render(text: str, detections: Sequence[Detection]) -> str:
27
+ """Render detector-selected spans without repeating matching or overlap selection."""
28
+ parts = []
29
+ cursor = 0
30
+ for index, match in enumerate(detections):
31
+ for value in (match.start, match.end, match.errors, match.missing_words):
32
+ if isinstance(value, bool) or not isinstance(value, int) or value < 0:
33
+ raise ValueError("Annotation offsets and costs must be nonnegative integers")
34
+ if not cursor <= match.start < match.end <= len(text):
35
+ raise ValueError("Annotation spans must be in bounds, ordered and non-overlapping")
36
+ if text[match.start : match.end] != match.text:
37
+ raise ValueError("Annotation span differs from original input text")
38
+ parts.append(_escape(text[cursor : match.start]))
39
+ parts.append(
40
+ f'<mark class="hadith-match" data-match-index="{index}" '
41
+ f'data-start="{match.start}" data-end="{match.end}" '
42
+ f'data-errors="{match.errors}" data-missing-words="{match.missing_words}" '
43
+ f'data-reference-count="{len(match.references)}">'
44
+ )
45
+ parts.append(_escape(text[match.start : match.end]))
46
+ parts.append("</mark>")
47
+ cursor = match.end
48
+ parts.append(_escape(text[cursor:]))
49
+ return "".join(parts)
@@ -0,0 +1,45 @@
1
+ {
2
+ "artifacts": {
3
+ "record-offsets.bin": "5ff1228d1d7cc82df5baa6853b510dca8d0897e165217d4df9bf3a26f16f32d1",
4
+ "records.jsonl": "9ec8e19aaab80ffbc01aa162122060099bc0a5de3841b544b4afa1c381414246",
5
+ "tokens.bin": "3488e5e47e5a91d42c2f84ba14560cf673e134081aea52a6ab28f08fdda2d317",
6
+ "trigram-offsets.bin": "c7edf613d74f00c806eafea7f0a6fd3f7dcd916c094f2c210d88b8380d6cbf6b",
7
+ "trigram-postings.bin": "0a246cc840d7fad97945ca2164545c72ad59a35245d87cb6aa7359fd6bea21cf",
8
+ "trigrams.marisa": "9bb9eae9fc858f7a5ed9a6675035f407b77f9c561e9cfd441cbcf0b14b49518e",
9
+ "unigram-offsets.bin": "3e8f5dc282137fc6f418f6bc33e2b0dbe987d860b4ffc5761944432a5f3f5d01",
10
+ "unigram-postings.bin": "752a095ec3f92dddbc90a751c6ed295f5b363035f76ba6479098d13750381b8a",
11
+ "vocabulary.marisa": "faf3d4b7d5d9952e5ea3823d305d29e46d4fcc1a6f52751cdada81a529c655cc"
12
+ },
13
+ "counts": {
14
+ "records": 14940,
15
+ "tokens": 1039600,
16
+ "trigram_keys": 382866,
17
+ "trigram_postings": 1009649,
18
+ "vocabulary": 40949
19
+ },
20
+ "notices": {
21
+ "LICENSES.md": "211ab9b99234f8b4c99f4e295cd29f0524d1a8508b10916c2ce84aa28ee4fde3",
22
+ "PROVENANCE.md": "f565245adb555fbe5b4d4a838369fd3c50e37de3f0302c5a0291c6a36c9517c7",
23
+ "README.md": "fc207de3c37457f4cd02075cdd1f76562905f7b5efe7cdaf3c0e2ae0a51bc9fc",
24
+ "SOURCES.md": "a395c79251f54b1b780a475c32480f968d277b71a04017af18227df3e9545e53",
25
+ "UPSTREAM-UNLICENSE.txt": "6b0382b16279f26ff69014300541967a356a666eb0b91b422f6862f6b7dad17e",
26
+ "collections.csv": "806aef24f330464c3b9bb0f2526ed1f962d12c6edecc8d0047633c324189f11b",
27
+ "configs.csv": "5c397a92a7449a568104ff2e6d6e60bdea68d7d0f19556591871be38693c210c"
28
+ },
29
+ "source_dataset": {
30
+ "configurations": [
31
+ "bukhari-ar",
32
+ "muslim-ar"
33
+ ],
34
+ "repository": "quranlab/hadith",
35
+ "revision": "cc6972dae1f46f5f5e2b65e877d24c4539bc4391"
36
+ },
37
+ "version": 1,
38
+ "versions": {
39
+ "index": 2,
40
+ "manifest": 1,
41
+ "normalization": 1,
42
+ "records": 1,
43
+ "tokens": 1
44
+ }
45
+ }
@@ -0,0 +1,16 @@
1
+ """Resolve preinstalled resources without extraction, downloads or construction."""
2
+
3
+ from pathlib import Path
4
+
5
+
6
+ def directory(index_dir: str | Path | None) -> Path:
7
+ if index_dir is not None:
8
+ return Path(index_dir).resolve()
9
+ bundled = Path(__file__).resolve().parent / "_data"
10
+ if not (bundled / "manifest.json").is_file():
11
+ raise FileNotFoundError(
12
+ "Bundled Hadith resources are missing. Reinstall the complete hadith-detector "
13
+ "distribution, or pass index_dir explicitly. Development checkouts must run "
14
+ "python -m tools.stage_bundle before using bundled resources."
15
+ )
16
+ return bundled
@@ -0,0 +1,84 @@
1
+ """One synchronized, lazily populated convenience detector cache."""
2
+
3
+ from pathlib import Path
4
+ from threading import RLock
5
+
6
+ from ._annotation import validate
7
+ from ._bundled import directory
8
+ from .config import DetectorConfig
9
+ from .detector import HadithDetector
10
+ from .models import Detection, HadithRecord
11
+
12
+ _lock = RLock()
13
+ _entry: tuple[Path, HadithDetector] | None = None
14
+
15
+
16
+ def _detector(index_dir: str | Path | None) -> HadithDetector:
17
+ global _entry
18
+ resolved = directory(index_dir)
19
+ with _lock:
20
+ if _entry is None or _entry[0] != resolved:
21
+ detector = HadithDetector(index_dir)
22
+ _entry = resolved, detector
23
+ return _entry[1]
24
+
25
+
26
+ def clear_cache() -> None:
27
+ """Forget the cached detector without interrupting any in-flight call."""
28
+ global _entry
29
+ with _lock:
30
+ _entry = None
31
+
32
+
33
+ def detect(
34
+ text: str,
35
+ *,
36
+ index_dir: str | Path | None = None,
37
+ config: DetectorConfig | None = None,
38
+ min_match_words: int | None = None,
39
+ max_errors: int | None = None,
40
+ max_missing_words: int | None = None,
41
+ ) -> list[Detection]:
42
+ """Detect using cached bundled/local resources and independent per-call defaults."""
43
+ if config is not None and not isinstance(config, DetectorConfig):
44
+ raise TypeError("config must be a DetectorConfig or None")
45
+ effective = (config if config is not None else DetectorConfig())._override(
46
+ min_match_words, max_errors, max_missing_words
47
+ )
48
+ return _detector(index_dir).detect(
49
+ text,
50
+ min_match_words=effective.min_match_words,
51
+ max_errors=effective.max_errors,
52
+ max_missing_words=effective.max_missing_words,
53
+ )
54
+
55
+
56
+ def annotate(
57
+ text: str,
58
+ *,
59
+ index_dir: str | Path | None = None,
60
+ format: str = "html",
61
+ config: DetectorConfig | None = None,
62
+ min_match_words: int | None = None,
63
+ max_errors: int | None = None,
64
+ max_missing_words: int | None = None,
65
+ ) -> str:
66
+ """Annotate plain text using the single cached detector and per-call settings."""
67
+ validate(text, format)
68
+ if config is not None and not isinstance(config, DetectorConfig):
69
+ raise TypeError("config must be a DetectorConfig or None")
70
+ effective = (config if config is not None else DetectorConfig())._override(
71
+ min_match_words, max_errors, max_missing_words
72
+ )
73
+ return _detector(index_dir).annotate(
74
+ text,
75
+ format=format,
76
+ min_match_words=effective.min_match_words,
77
+ max_errors=effective.max_errors,
78
+ max_missing_words=effective.max_missing_words,
79
+ )
80
+
81
+
82
+ def get_hadith(hadith_key: str, *, index_dir: str | Path | None = None) -> HadithRecord:
83
+ """Retrieve an isolated source snapshot using the single cached detector."""
84
+ return _detector(index_dir).get_hadith(hadith_key)
@@ -0,0 +1 @@
1
+ {"files":{"manifest.json":"a34ebdd099389594b039a4ac55e667081110c2c192f43eee08095fe1f7d5dcbd","notices/LICENSES.md":"211ab9b99234f8b4c99f4e295cd29f0524d1a8508b10916c2ce84aa28ee4fde3","notices/PROVENANCE.md":"f565245adb555fbe5b4d4a838369fd3c50e37de3f0302c5a0291c6a36c9517c7","notices/README.md":"fc207de3c37457f4cd02075cdd1f76562905f7b5efe7cdaf3c0e2ae0a51bc9fc","notices/SOURCES.md":"a395c79251f54b1b780a475c32480f968d277b71a04017af18227df3e9545e53","notices/UPSTREAM-UNLICENSE.txt":"6b0382b16279f26ff69014300541967a356a666eb0b91b422f6862f6b7dad17e","notices/collections.csv":"806aef24f330464c3b9bb0f2526ed1f962d12c6edecc8d0047633c324189f11b","notices/configs.csv":"5c397a92a7449a568104ff2e6d6e60bdea68d7d0f19556591871be38693c210c","record-offsets.bin":"5ff1228d1d7cc82df5baa6853b510dca8d0897e165217d4df9bf3a26f16f32d1","records.jsonl":"9ec8e19aaab80ffbc01aa162122060099bc0a5de3841b544b4afa1c381414246","tokens.bin":"3488e5e47e5a91d42c2f84ba14560cf673e134081aea52a6ab28f08fdda2d317","trigram-offsets.bin":"c7edf613d74f00c806eafea7f0a6fd3f7dcd916c094f2c210d88b8380d6cbf6b","trigram-postings.bin":"0a246cc840d7fad97945ca2164545c72ad59a35245d87cb6aa7359fd6bea21cf","trigrams.marisa":"9bb9eae9fc858f7a5ed9a6675035f407b77f9c561e9cfd441cbcf0b14b49518e","unigram-offsets.bin":"3e8f5dc282137fc6f418f6bc33e2b0dbe987d860b4ffc5761944432a5f3f5d01","unigram-postings.bin":"752a095ec3f92dddbc90a751c6ed295f5b363035f76ba6479098d13750381b8a","vocabulary.marisa":"faf3d4b7d5d9952e5ea3823d305d29e46d4fcc1a6f52751cdada81a529c655cc"},"lock_sha256":"1f4711afa95bda7cd5ce69d54ca804553e53d0b51ef64611dd796f2b219f593d","version":1}
@@ -0,0 +1 @@
1
+ {"algorithm":"trigram+unigram","artifacts":{"record-offsets.bin":"5ff1228d1d7cc82df5baa6853b510dca8d0897e165217d4df9bf3a26f16f32d1","records.jsonl":"9ec8e19aaab80ffbc01aa162122060099bc0a5de3841b544b4afa1c381414246","tokens.bin":"3488e5e47e5a91d42c2f84ba14560cf673e134081aea52a6ab28f08fdda2d317","trigram-offsets.bin":"c7edf613d74f00c806eafea7f0a6fd3f7dcd916c094f2c210d88b8380d6cbf6b","trigram-postings.bin":"0a246cc840d7fad97945ca2164545c72ad59a35245d87cb6aa7359fd6bea21cf","trigrams.marisa":"9bb9eae9fc858f7a5ed9a6675035f407b77f9c561e9cfd441cbcf0b14b49518e","unigram-offsets.bin":"3e8f5dc282137fc6f418f6bc33e2b0dbe987d860b4ffc5761944432a5f3f5d01","unigram-postings.bin":"752a095ec3f92dddbc90a751c6ed295f5b363035f76ba6479098d13750381b8a","vocabulary.marisa":"faf3d4b7d5d9952e5ea3823d305d29e46d4fcc1a6f52751cdada81a529c655cc"},"build_fingerprint":"e92ba58375816eb02ebb26c9f66a9cb15add35e990037e1b6444864ba7ce344b","builder":{"dependency_lock_sha256":"8ef4b3b8de07c227551b845eb52ef46d47ff7b618f153c3cc9ebd157aa1666ce","marisa_trie":"1.4.1","native_settings":{"binary":false,"cache_size":512,"num_tries":3,"order":131072},"python":"3.14.6","sources":{"src/hadith_detector/_marisa.py":"a3b7800da1923af9aef7a16127ae92a8aa4a6506bfc89540a9cc8b93f5a9a6b1","src/hadith_detector/_resources.py":"f7e9c2e124ba1fec9c255fbf89fe1f0590e82f827237f939d3bb854e0ca92a59","src/hadith_detector/_text.py":"825013da3b680c286c824d9915668618a9fa2aab3afab93f9eea85ae813a1571","tools/build_index.py":"3752db36fcd43a107edec828e253ffd76925b282a103d077ec638b2f5b6dfec5","tools/exact_benchmark/corpus.py":"5ccd73de6eb35a41e2df0f95ddc37550f9a7649282a928b5b5489902a472bc09","tools/exact_benchmark/indexes.py":"38266cdffc92f383b6d728cabb6c38699fae5b0177a1b2e9f60f4abb9d9c2a1c"},"unicode":"16.0.0"},"byte_order":"little","counts":{"records":14940,"tokens":1039600,"trigram_keys":382866,"trigram_postings":1009649,"vocabulary":40949},"format":"hadith-detector-index","prepared_manifest_sha256":"f0ee8509b911b7835771de94fe65a97a17098ebba53de57f9c274dd077a5a0e2","source_artifacts":{"records.jsonl":"9ec8e19aaab80ffbc01aa162122060099bc0a5de3841b544b4afa1c381414246","tokens.jsonl":"ee07fce76d7862854e069ef81d72255cf3cb616daad9d0c57977e287a4e11f33","validation.json":"0e2187d3b4e7abb19de4648de54ff5ec79c595893fc5a8ef659dd8ed5baadbb1"},"source_dataset":{"configurations":["bukhari-ar","muslim-ar"],"repository":"quranlab/hadith","revision":"cc6972dae1f46f5f5e2b65e877d24c4539bc4391"},"source_fingerprint":"87fd7b7824fe60063026a582a845082f7fc6fe88587cb1d0805dbae372ea01ed","versions":{"index":2,"manifest":1,"normalization":1,"records":1,"tokens":1}}
@@ -0,0 +1,17 @@
1
+ # Licenses, Attribution & Removal (NOTICE) — quranlab/hadith
2
+
3
+ Binding terms are **per row** (`license`, `source`, `attribution`). Summary below.
4
+
5
+ ## Theology gate
6
+ Strictly **Ahl al-Sunnah wa'l-Jamāʿah**. Excluded by design: the Shia Four Books (al-Kāfī, Man lā yaḥḍuruhu al-Faqīh, Tahdhīb al-Aḥkām, al-Istibṣār) and other Shia/Ismāʿīlī collections; Ibāḍī (Musnad al-Rabīʿ b. Ḥabīb); Ahmadiyya/Qādiānī; Quranist/hadith-rejecter sources; hostile-orientalist grade framings.
7
+
8
+ ## Sources & tiers
9
+ - **Arabic matn (65,834 hadith across 13 collections):** public domain; digitisation from `mhashim6/Open-Hadith-Data`, ODbL-1.0 + DbCL-1.0. Attribution required.
10
+ - **Graded multilingual layer (72 languages):** HadeethEnc.com (IslamHouse / Saudi MoIA) — verbatim, version-pinned, with grade + attribution; reproduced under their open-with-attribution terms; removal on request.
11
+ - **Copyrighted full-collection translations (roadmap):** by reference + full attribution under **notice-and-takedown** (removal on rights-holder request via the HF Community tab).
12
+
13
+ ## Grade integrity
14
+ Grades are **grader-attributed, never asserted by QuranLab**; the matn layer is ungraded; collection-level status lives in the `collections` config, never auto-stamped per row.
15
+
16
+ ## Removal
17
+ Open the dataset's **Community (Discussions)** tab to request removal of any row.
@@ -0,0 +1,46 @@
1
+ # Bundled corpus provenance and redistribution record
2
+
3
+ This package includes the Arabic `bukhari-ar` and `muslim-ar` configurations from
4
+ QuranLab Hadith revision `cc6972dae1f46f5f5e2b65e877d24c4539bc4391`: 7,580 and
5
+ 7,360 records respectively. Original fields, identities, duplicate narrations,
6
+ nullable URLs and source text are preserved. Tokens and the prebuilt MARISA v2
7
+ index are derived resources; normalization version is 1.
8
+
9
+ Credit **QuranLab** (<https://huggingface.co/datasets/quranlab/hadith>) and
10
+ **fawazahmed0/hadith-api** (<https://github.com/fawazahmed0/hadith-api>).
11
+ The pinned `configs.csv` attributes both selected configurations to hadith-api,
12
+ describes classical Arabic text as public domain, and marks commercial use as
13
+ allowed. Upstream's Unlicense permits copying, modification and redistribution.
14
+ Its preserved text was retrieved at commit
15
+ `df57907be35291c91ad6a6691180e22ca9920784`; this pins the licensing evidence, not
16
+ the unknown upstream data revision used by QuranLab.
17
+
18
+ ## Distinguishing the available evidence
19
+
20
+ The broad QuranLab `LICENSES.md` summary attributes Arabic material to
21
+ Open-Hadith-Data and mentions ODbL/DbCL. The selected configuration rows instead
22
+ identify hadith-api. `SOURCES.md` identifies Open-Hadith-Data with Ahmad/Darimi,
23
+ which are not bundled. Both notices are preserved verbatim, alongside the full
24
+ pinned configuration and collection metadata. This package follows the selected
25
+ configurations' attribution and the maintainer's approved Phase 2 provenance
26
+ decision; the broad summary is not rewritten or presented as consistent.
27
+
28
+ Only the selected Arabic records are redistributed: no translations, HadeethEnc
29
+ text, Ahmad/Darimi records, or text scraped from sunnah.com. Source links and
30
+ numbering are retained as supplied; links do not establish rights to linked pages.
31
+ No independent authentication or grader endorsement is asserted.
32
+
33
+ The project code is MIT licensed. This does not relicense third-party data.
34
+ `UPSTREAM-UNLICENSE.txt`, QuranLab `README.md`, `LICENSES.md`, `SOURCES.md`,
35
+ `configs.csv` and `collections.csv` accompany the data. Preserve these notices
36
+ when redistributing this bundle. Consult the original notices for attribution,
37
+ correction and removal contact information.
38
+
39
+ ## Integrity and build provenance
40
+
41
+ `_bundle.json` records approved resource payload hashes, counts, versions and
42
+ source revision. `_data/manifest.json` records source preparation and builder
43
+ fingerprints; `_data/bundle.json` records every staged file and its checksum.
44
+ The builder manifest may differ when code or environment provenance changes,
45
+ while approved resource payloads must remain byte-identical. No runtime resource
46
+ construction or download is performed.