pdacek 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.
pdacek/__init__.py ADDED
@@ -0,0 +1,116 @@
1
+ """pdacek - a plain, notebook-friendly NLP lab toolkit.
2
+
3
+ Five self-contained experiments, each available three ways:
4
+
5
+ * run it inline -> ``pdacek.p3()``
6
+ * read the code -> ``print(pdacek.source(3))``
7
+ * write it to disk -> ``pdacek.save(3)``
8
+
9
+ The functions are also importable individually, e.g.
10
+ ``from pdacek import min_edit_distance``.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from pathlib import Path
16
+ from typing import Dict, List, Optional
17
+
18
+ from .programs import PROGRAMS, numbers, run, source, titles
19
+ from .programs.pl01_preprocessing import (
20
+ filtration,
21
+ preprocess,
22
+ stopword_removal,
23
+ stemming,
24
+ tokenization,
25
+ validation,
26
+ )
27
+ from .programs.pl02_ngrams import build_corpus, ngram_counts, ngram_probabilities
28
+ from .programs.pl03_edit_distance import edit_table, med, min_edit_distance, trace_operations
29
+ from .programs.pl04_parsing import Parser
30
+ from .programs.pl05_naive_bayes import NaiveBayes
31
+
32
+ __version__ = "0.1.0"
33
+
34
+ __all__ = [
35
+ "__version__",
36
+ "programs",
37
+ "run",
38
+ "source",
39
+ "save",
40
+ "p1",
41
+ "p2",
42
+ "p3",
43
+ "p4",
44
+ "p5",
45
+ # helpers
46
+ "tokenization",
47
+ "filtration",
48
+ "validation",
49
+ "stopword_removal",
50
+ "stemming",
51
+ "preprocess",
52
+ "build_corpus",
53
+ "ngram_counts",
54
+ "ngram_probabilities",
55
+ "edit_table",
56
+ "med",
57
+ "min_edit_distance",
58
+ "trace_operations",
59
+ "Parser",
60
+ "NaiveBayes",
61
+ ]
62
+
63
+
64
+ def programs() -> Dict[int, str]:
65
+ """Return ``{number: title}`` for every bundled experiment."""
66
+ return titles()
67
+
68
+
69
+ def save(number: int, directory: "str | Path" = ".") -> Path:
70
+ """Write a program listing to ``directory`` and return its path."""
71
+ program = PROGRAMS[int(number)]
72
+ target = Path(directory) / program.script_name
73
+ target.parent.mkdir(parents=True, exist_ok=True)
74
+ target.write_text(source(number), encoding="utf-8")
75
+ return target
76
+
77
+
78
+ def _run_all() -> List[int]:
79
+ for number in numbers():
80
+ run(number)
81
+ print()
82
+ return numbers()
83
+
84
+
85
+ def p1() -> None:
86
+ """Program 1 - text preprocessing pipeline."""
87
+ run(1)
88
+
89
+
90
+ def p2() -> None:
91
+ """Program 2 - unigram, bigram, trigram probabilities."""
92
+ run(2)
93
+
94
+
95
+ def p3() -> None:
96
+ """Program 3 - minimum edit distance."""
97
+ run(3)
98
+
99
+
100
+ def p4() -> None:
101
+ """Program 4 - top-down and bottom-up parsing."""
102
+ run(4)
103
+
104
+
105
+ def p5() -> None:
106
+ """Program 5 - add-one smoothed Naive Bayes."""
107
+ run(5)
108
+
109
+
110
+ def __getattr__(name: str) -> Optional[object]: # pragma: no cover - tiny shim
111
+ """Allow ``pdacek.p6`` style access to fail with a clear message."""
112
+ if name.startswith("p") and name[1:].isdigit():
113
+ raise AttributeError(
114
+ f"no program {name[1:]} in this release; available: {numbers()}"
115
+ )
116
+ raise AttributeError(f"module 'pdacek' has no attribute {name!r}")
pdacek/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Support ``python -m pdacek``."""
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(main())
pdacek/cli.py ADDED
@@ -0,0 +1,98 @@
1
+ """Command line entry point: ``pdacek 3``, ``pdacek --list``, ..."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+ from typing import List, Optional
8
+
9
+ from . import __version__, numbers, programs, run, save, source
10
+
11
+
12
+ def build_parser() -> argparse.ArgumentParser:
13
+ parser = argparse.ArgumentParser(
14
+ prog="pdacek",
15
+ description="Run the bundled NLP lab experiments without opening an editor.",
16
+ formatter_class=argparse.RawDescriptionHelpFormatter,
17
+ epilog=(
18
+ "examples:\n"
19
+ " pdacek run every experiment\n"
20
+ " pdacek 3 run experiment 3\n"
21
+ " pdacek p3 same thing\n"
22
+ " pdacek 3 --source print the listing instead of running it\n"
23
+ " pdacek 3 --save write the listing to the current directory\n"
24
+ ),
25
+ )
26
+ parser.add_argument(
27
+ "program",
28
+ nargs="?",
29
+ help="experiment number (1-5) or name such as p3",
30
+ )
31
+ parser.add_argument("--list", "-l", action="store_true", help="list experiments and exit")
32
+ parser.add_argument("--source", "-s", action="store_true", help="print the listing")
33
+ parser.add_argument(
34
+ "--save",
35
+ nargs="?",
36
+ const=".",
37
+ metavar="DIR",
38
+ help="write the listing to DIR (default: current directory)",
39
+ )
40
+ parser.add_argument("--version", "-V", action="version", version=f"pdacek {__version__}")
41
+ return parser
42
+
43
+
44
+ def _parse_number(raw: Optional[str]) -> Optional[int]:
45
+ if raw is None:
46
+ return None
47
+ cleaned = raw.strip().lower().lstrip("p").replace("program", "").strip(" _-")
48
+ try:
49
+ return int(cleaned)
50
+ except ValueError:
51
+ return None
52
+
53
+
54
+ def list_programs() -> None:
55
+ print(f"pdacek {__version__} - bundled experiments\n")
56
+ for number, title in programs().items():
57
+ print(f" {number}. {title}")
58
+ print("\nRun one with: pdacek <number>")
59
+
60
+
61
+ def main(argv: Optional[List[str]] = None) -> int:
62
+ args = build_parser().parse_args(argv)
63
+
64
+ if args.list:
65
+ list_programs()
66
+ return 0
67
+
68
+ number = _parse_number(args.program)
69
+ if args.program is not None and number is None:
70
+ print(f"error: could not read a program number from {args.program!r}", file=sys.stderr)
71
+ print(f"available: {', '.join(str(n) for n in numbers())}", file=sys.stderr)
72
+ return 2
73
+
74
+ try:
75
+ if number is None:
76
+ for n in numbers():
77
+ run(n)
78
+ print()
79
+ return 0
80
+
81
+ if args.save is not None:
82
+ path = save(number, args.save)
83
+ print(f"wrote {path}")
84
+ return 0
85
+
86
+ if args.source:
87
+ print(source(number))
88
+ return 0
89
+
90
+ run(number)
91
+ return 0
92
+ except KeyError as exc:
93
+ print(f"error: {exc.args[0]}", file=sys.stderr)
94
+ return 2
95
+
96
+
97
+ if __name__ == "__main__": # pragma: no cover
98
+ raise SystemExit(main())
pdacek/compat.py ADDED
@@ -0,0 +1,129 @@
1
+ """Shared helpers: tokenisation and NLTK corpus access with safe fallbacks.
2
+
3
+ The lab machines this toolkit targets are often offline, so every helper here
4
+ degrades to a pure-``re`` implementation instead of raising when an NLTK
5
+ resource (``punkt``, ``stopwords``) has not been downloaded yet.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from typing import List
12
+
13
+ try: # NLTK is a declared dependency, but keep the package importable without it.
14
+ import nltk
15
+ except ImportError: # pragma: no cover - defensive
16
+ nltk = None # type: ignore[assignment]
17
+
18
+ __all__ = ["tokenize", "stopwords_en", "stemmer", "ensure_punkt", "nltk_available"]
19
+
20
+ _TOKEN_RE = re.compile(r"[A-Za-z]+(?:'[A-Za-z]+)?|\d+|[^\sA-Za-z\d]")
21
+
22
+ # Small built-in list, used only when the NLTK corpus is unavailable.
23
+ _FALLBACK_STOPWORDS = frozenset(
24
+ """
25
+ a about above after again against all am an and any are as at be because been
26
+ before being below between both but by can did do does doing down during each
27
+ few for from further had has have having he her here hers herself him himself
28
+ his how i if in into is it its itself just me more most my myself no nor not
29
+ now of off on once only or other our ours ourselves out over own same she
30
+ should so some such than that the their theirs them themselves then there
31
+ these they this those through to too under until up very was we were what
32
+ when where which while who whom why will with you your yours yourself
33
+ yourselves
34
+ """.split()
35
+ )
36
+
37
+
38
+ def nltk_available() -> bool:
39
+ """Return ``True`` when NLTK can be imported."""
40
+ return nltk is not None
41
+
42
+
43
+ def _resource_present(resource: str, package: str) -> bool:
44
+ if nltk is None:
45
+ return False
46
+ try:
47
+ nltk.data.find(f"{package}/{resource}")
48
+ return True
49
+ except LookupError:
50
+ return False
51
+
52
+
53
+ def ensure_punkt() -> bool:
54
+ """Make sure a sentence/word tokenizer model exists; return availability.
55
+
56
+ NLTK 3.9 renamed the tokenizer data to ``punkt_tab``; both names are tried
57
+ and any download failure (offline machine) is swallowed on purpose.
58
+ """
59
+ if nltk is None:
60
+ return False
61
+ for resource in ("punkt", "punkt_tab"):
62
+ if _resource_present(resource, "tokenizers"):
63
+ return True
64
+ try:
65
+ nltk.download(resource, quiet=True)
66
+ except Exception: # pragma: no cover - network/permission problems
67
+ pass
68
+ if _resource_present(resource, "tokenizers"):
69
+ return True
70
+ return False
71
+
72
+
73
+ def tokenize(text: str) -> List[str]:
74
+ """Split ``text`` into word and punctuation tokens.
75
+
76
+ Uses NLTK's ``word_tokenize`` when the model is available, otherwise a
77
+ regex tokenizer that keeps punctuation as its own token, which is what the
78
+ filtration and script-validation steps in Program 1 need.
79
+ """
80
+ if ensure_punkt():
81
+ try:
82
+ return nltk.word_tokenize(text) # type: ignore[union-attr]
83
+ except LookupError: # pragma: no cover - stale resource index
84
+ pass
85
+ return _TOKEN_RE.findall(text)
86
+
87
+
88
+ def stopwords_en() -> frozenset:
89
+ """Return the English stop-word set (NLTK corpus when available)."""
90
+ if nltk is not None:
91
+ try:
92
+ return frozenset(nltk.corpus.stopwords.words("english")) # type: ignore[union-attr]
93
+ except LookupError:
94
+ try:
95
+ nltk.download("stopwords", quiet=True) # type: ignore[union-attr]
96
+ return frozenset(nltk.corpus.stopwords.words("english")) # type: ignore[union-attr]
97
+ except Exception:
98
+ pass
99
+ return _FALLBACK_STOPWORDS
100
+
101
+
102
+ class _SimpleStemmer:
103
+ """Very small suffix stripper used when NLTK is not importable."""
104
+
105
+ _SUFFIXES = (
106
+ "ational", "ization", "iveness", "ingly", "edly", "ments",
107
+ "ing", "ies", "ied", "ed", "es", "s",
108
+ )
109
+
110
+ def stem(self, word: str) -> str:
111
+ low = word.lower()
112
+ for suffix in self._SUFFIXES:
113
+ if len(low) > len(suffix) + 2 and low.endswith(suffix):
114
+ if suffix == "ies":
115
+ return low[: -len(suffix)] + "y"
116
+ return low[: -len(suffix)]
117
+ return low
118
+
119
+
120
+ def stemmer():
121
+ """Return a Porter stemmer (NLTK) or a minimal fallback stemmer."""
122
+ if nltk is not None:
123
+ try:
124
+ from nltk.stem import PorterStemmer
125
+
126
+ return PorterStemmer()
127
+ except Exception: # pragma: no cover
128
+ pass
129
+ return _SimpleStemmer()
@@ -0,0 +1,107 @@
1
+ """Program modules and the registry that maps numbers to them."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from importlib import resources
7
+ from types import ModuleType
8
+ from typing import Dict, List
9
+
10
+ from . import (
11
+ pl01_preprocessing,
12
+ pl02_ngrams,
13
+ pl03_edit_distance,
14
+ pl04_parsing,
15
+ pl05_naive_bayes,
16
+ )
17
+
18
+ __all__ = ["Program", "PROGRAMS", "get", "run", "source", "titles", "numbers"]
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class Program:
23
+ """Metadata for one lab program."""
24
+
25
+ number: int
26
+ slug: str
27
+ title: str
28
+ module: ModuleType
29
+
30
+ @property
31
+ def script_name(self) -> str:
32
+ return f"{self.slug}.py"
33
+
34
+ def script(self) -> str:
35
+ """Return the standalone, paste-into-a-notebook listing."""
36
+ return (
37
+ resources.files("pdacek")
38
+ .joinpath("scripts", self.script_name)
39
+ .read_text(encoding="utf-8")
40
+ )
41
+
42
+ def run(self, *args, **kwargs) -> None:
43
+ self.module.main(*args, **kwargs)
44
+
45
+
46
+ PROGRAMS: Dict[int, Program] = {
47
+ 1: Program(
48
+ 1,
49
+ "pl01_preprocessing",
50
+ "Text preprocessing: tokenisation, filtration, script validation, "
51
+ "stop-word removal, stemming",
52
+ pl01_preprocessing,
53
+ ),
54
+ 2: Program(
55
+ 2,
56
+ "pl02_ngrams",
57
+ "Unigram, bigram and trigram probability estimates",
58
+ pl02_ngrams,
59
+ ),
60
+ 3: Program(
61
+ 3,
62
+ "pl03_edit_distance",
63
+ "Minimum edit distance with test cases and a DP table",
64
+ pl03_edit_distance,
65
+ ),
66
+ 4: Program(
67
+ 4,
68
+ "pl04_parsing",
69
+ "Top-down and bottom-up parsing from a context-free grammar",
70
+ pl04_parsing,
71
+ ),
72
+ 5: Program(
73
+ 5,
74
+ "pl05_naive_bayes",
75
+ "Add-one smoothed Naive Bayes movie-review classification",
76
+ pl05_naive_bayes,
77
+ ),
78
+ }
79
+
80
+
81
+ def numbers() -> List[int]:
82
+ """Available program numbers, ascending."""
83
+ return sorted(PROGRAMS)
84
+
85
+
86
+ def titles() -> Dict[int, str]:
87
+ """Mapping of program number to one-line title."""
88
+ return {n: p.title for n, p in sorted(PROGRAMS.items())}
89
+
90
+
91
+ def get(number: int) -> Program:
92
+ """Look up a program by number, with a helpful error message."""
93
+ try:
94
+ return PROGRAMS[int(number)]
95
+ except (KeyError, TypeError, ValueError):
96
+ available = ", ".join(str(n) for n in numbers())
97
+ raise KeyError(f"unknown program {number!r}; available: {available}") from None
98
+
99
+
100
+ def run(number: int) -> None:
101
+ """Execute one program and print its output."""
102
+ get(number).run()
103
+
104
+
105
+ def source(number: int) -> str:
106
+ """Return the standalone script for one program."""
107
+ return get(number).script()
@@ -0,0 +1,80 @@
1
+ """Program 1 - text preprocessing pipeline.
2
+
3
+ Tokenisation -> filtration -> script validation -> stop-word removal ->
4
+ stemming, exposed as small composable steps plus a one-call ``preprocess``.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ from typing import Dict, List
11
+
12
+ from ..compat import stemmer, stopwords_en, tokenize
13
+
14
+ SAMPLE_TEXT = (
15
+ "Natural Language Processing is a fascinating field of AI.\n"
16
+ "It involves processing human languages, like English!"
17
+ )
18
+
19
+ _NON_ALPHA = re.compile(r"[^a-zA-Z]")
20
+ _ALPHA_ONLY = re.compile(r"^[a-zA-Z]+$")
21
+
22
+
23
+ def tokenization(text: str = SAMPLE_TEXT) -> List[str]:
24
+ """Step 1 - split raw text into tokens, punctuation included."""
25
+ return tokenize(text)
26
+
27
+
28
+ def filtration(tokens: List[str]) -> List[str]:
29
+ """Step 2 - drop non-alphabetic characters, then drop empty strings."""
30
+ stripped = [_NON_ALPHA.sub("", w) for w in tokens]
31
+ return [w for w in stripped if w]
32
+
33
+
34
+ def validation(tokens: List[str]) -> List[str]:
35
+ """Step 3 - keep only tokens that are pure ASCII English letters."""
36
+ return [w for w in tokens if _ALPHA_ONLY.match(w)]
37
+
38
+
39
+ def stopword_removal(tokens: List[str]) -> List[str]:
40
+ """Step 4 - remove English stop words (case-insensitive)."""
41
+ stop = stopwords_en()
42
+ return [w for w in tokens if w.lower() not in stop]
43
+
44
+
45
+ def stemming(tokens: List[str]) -> List[str]:
46
+ """Step 5 - Porter stemming."""
47
+ porter = stemmer()
48
+ return [porter.stem(w) for w in tokens]
49
+
50
+
51
+ def preprocess(text: str = SAMPLE_TEXT) -> Dict[str, List[str]]:
52
+ """Run the full pipeline and return every intermediate stage."""
53
+ tokens = tokenization(text)
54
+ filtered = filtration(tokens)
55
+ validated = validation(filtered)
56
+ cleaned = stopword_removal(validated)
57
+ stemmed = stemming(cleaned)
58
+ return {
59
+ "tokens": tokens,
60
+ "filtered": filtered,
61
+ "validated": validated,
62
+ "cleaned": cleaned,
63
+ "stemmed": stemmed,
64
+ }
65
+
66
+
67
+ def main(text: str = SAMPLE_TEXT) -> None:
68
+ """Reproduce the lab-sheet output."""
69
+ stages = preprocess(text)
70
+
71
+ print("Original Text:\n", text)
72
+ print("\nTokens:\n", stages["tokens"])
73
+ print("\nFiltered Tokens:\n", stages["filtered"])
74
+ print("\nValidated Tokens (English Only):\n", stages["validated"])
75
+ print("\nTokens after Stop Word Removal:\n", stages["cleaned"])
76
+ print("\nStemmed Tokens:\n", stages["stemmed"])
77
+
78
+
79
+ if __name__ == "__main__": # pragma: no cover
80
+ main()
@@ -0,0 +1,66 @@
1
+ """Program 2 - unigram, bigram and trigram probability estimates.
2
+
3
+ Unigram probabilities use maximum likelihood over the whole corpus; higher
4
+ orders use the conditional estimate ``C(w1..wn) / C(w1..wn-1)``.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections import Counter
10
+ from typing import Dict, Iterable, List, Sequence, Tuple
11
+
12
+ from ..compat import tokenize
13
+
14
+ SENTENCES: List[str] = [
15
+ "The quick brown fox jumps over the lazy dog",
16
+ "The dog barks at the fox",
17
+ "The fox is quick and the dog is lazy",
18
+ "The quick dog jumps over the lazy fox",
19
+ ]
20
+
21
+
22
+ def _ngrams(sequence: Sequence[str], n: int) -> Iterable[Tuple[str, ...]]:
23
+ if n <= 0:
24
+ raise ValueError("n must be >= 1")
25
+ for i in range(len(sequence) - n + 1):
26
+ yield tuple(sequence[i : i + n])
27
+
28
+
29
+ def build_corpus(sentences: Sequence[str] = SENTENCES) -> List[List[str]]:
30
+ """Lower-case and tokenise every sentence."""
31
+ return [tokenize(s.lower()) for s in sentences]
32
+
33
+
34
+ def ngram_counts(corpus: Sequence[Sequence[str]], n: int) -> Counter:
35
+ """Count every n-gram in the corpus."""
36
+ return Counter(g for sentence in corpus for g in _ngrams(sentence, n))
37
+
38
+
39
+ def ngram_probabilities(corpus: Sequence[Sequence[str]], n: int) -> Dict[Tuple[str, ...], float]:
40
+ """Return ``{ngram: probability}`` for the requested order ``n``."""
41
+ counts = ngram_counts(corpus, n)
42
+ if n == 1:
43
+ total = sum(counts.values())
44
+ return {gram: count / total for gram, count in counts.items()}
45
+
46
+ previous = ngram_counts(corpus, n - 1)
47
+ probabilities: Dict[Tuple[str, ...], float] = {}
48
+ for gram, count in counts.items():
49
+ denominator = previous.get(gram[:-1], 0)
50
+ if denominator:
51
+ probabilities[gram] = count / denominator
52
+ return probabilities
53
+
54
+
55
+ def main(sentences: Sequence[str] = SENTENCES) -> None:
56
+ """Reproduce the lab-sheet output for n = 1, 2, 3."""
57
+ corpus = build_corpus(sentences)
58
+ for n in (1, 2, 3):
59
+ probabilities = ngram_probabilities(corpus, n)
60
+ print(f"\n{n}-Gram Probabilities:")
61
+ for gram, probability in probabilities.items():
62
+ print(f"{' '.join(gram)}: {probability:.4f}")
63
+
64
+
65
+ if __name__ == "__main__": # pragma: no cover
66
+ main()
@@ -0,0 +1,89 @@
1
+ """Program 3 - minimum edit distance (Levenshtein, unit cost).
2
+
3
+ The dynamic-programming table is kept so it can be printed like the lab sheet.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import List, Sequence, Tuple
9
+
10
+ TESTS: List[Tuple[str, str]] = [
11
+ ("kitten", "sitting"),
12
+ ("flaw", "lawn"),
13
+ ("intention", "execution"),
14
+ ("apple", "aple"),
15
+ ("", "hello"),
16
+ ("hello", ""),
17
+ ]
18
+
19
+
20
+ def edit_table(a: str, b: str) -> List[List[int]]:
21
+ """Return the full ``(len(a)+1) x (len(b)+1)`` distance matrix."""
22
+ m, n = len(a), len(b)
23
+ dp = [[0] * (n + 1) for _ in range(m + 1)]
24
+
25
+ for i in range(m + 1):
26
+ dp[i][0] = i
27
+ for j in range(n + 1):
28
+ dp[0][j] = j
29
+
30
+ for i in range(1, m + 1):
31
+ for j in range(1, n + 1):
32
+ if a[i - 1] == b[j - 1]:
33
+ dp[i][j] = dp[i - 1][j - 1]
34
+ else:
35
+ dp[i][j] = 1 + min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1])
36
+ return dp
37
+
38
+
39
+ def min_edit_distance(a: str, b: str) -> int:
40
+ """Levenshtein distance between ``a`` and ``b``."""
41
+ return edit_table(a, b)[len(a)][len(b)]
42
+
43
+
44
+ def med(a: str, b: str) -> Tuple[int, List[List[int]]]:
45
+ """Lab-compatible wrapper returning ``(distance, table)``."""
46
+ dp = edit_table(a, b)
47
+ return dp[len(a)][len(b)], dp
48
+
49
+
50
+ def display(dp: Sequence[Sequence[int]], a: str, b: str) -> None:
51
+ """Print the matrix with source/target letters as headers."""
52
+ print("\nEdit Distance Table:")
53
+ print(" " + " ".join("#" + c for c in " " + b))
54
+ for i, row in enumerate(dp):
55
+ print(("#" if i == 0 else a[i - 1]), *row)
56
+
57
+
58
+ def trace_operations(a: str, b: str) -> List[str]:
59
+ """Backtrace the table into edit operations (useful for the viva)."""
60
+ dp = edit_table(a, b)
61
+ i, j = len(a), len(b)
62
+ operations: List[str] = []
63
+ while i > 0 or j > 0:
64
+ if i > 0 and j > 0 and a[i - 1] == b[j - 1] and dp[i][j] == dp[i - 1][j - 1]:
65
+ operations.append(f"keep '{a[i - 1]}'")
66
+ i, j = i - 1, j - 1
67
+ elif i > 0 and j > 0 and dp[i][j] == dp[i - 1][j - 1] + 1:
68
+ operations.append(f"substitute '{a[i - 1]}' -> '{b[j - 1]}'")
69
+ i, j = i - 1, j - 1
70
+ elif i > 0 and dp[i][j] == dp[i - 1][j] + 1:
71
+ operations.append(f"delete '{a[i - 1]}'")
72
+ i -= 1
73
+ else:
74
+ operations.append(f"insert '{b[j - 1]}'")
75
+ j -= 1
76
+ return list(reversed(operations))
77
+
78
+
79
+ def main(tests: Sequence[Tuple[str, str]] = TESTS) -> None:
80
+ """Reproduce the lab-sheet output."""
81
+ for a, b in tests:
82
+ distance, dp = med(a, b)
83
+ print(f"\nComparing: '{a}' -> '{b}'")
84
+ print("Minimum Edit Distance:", distance)
85
+ display(dp, a, b)
86
+
87
+
88
+ if __name__ == "__main__": # pragma: no cover
89
+ main()
@@ -0,0 +1,150 @@
1
+ """Program 4 - top-down and bottom-up parsing from a context-free grammar.
2
+
3
+ ``Parser`` takes the grammar as ``(lhs, rhs)`` pairs where ``rhs`` is a list of
4
+ symbols; symbols that never appear on a left-hand side are terminals.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Dict, List, Optional, Sequence, Tuple
10
+
11
+ Grammar = Sequence[Tuple[str, Sequence[str]]]
12
+
13
+ GRAMMAR: List[Tuple[str, List[str]]] = [
14
+ ("S", ["NP", "VP"]),
15
+ ("NP", ["Det", "N"]),
16
+ ("VP", ["V", "NP"]),
17
+ ("VP", ["V"]),
18
+ ("Det", ["a"]),
19
+ ("Det", ["the"]),
20
+ ("N", ["cat"]),
21
+ ("N", ["dog"]),
22
+ ("V", ["chases"]),
23
+ ("V", ["eats"]),
24
+ ]
25
+
26
+ SENTENCE: List[str] = ["the", "cat", "chases", "a", "dog"]
27
+
28
+
29
+ class Parser:
30
+ """A tiny CFG parser with both search directions."""
31
+
32
+ def __init__(self, grammar: Grammar, start: str = "S") -> None:
33
+ self.rules: Dict[str, List[List[str]]] = {}
34
+ for lhs, rhs in grammar:
35
+ self.rules.setdefault(lhs, []).append(list(rhs))
36
+ self.start = start
37
+
38
+ # ------------------------------------------------------------------ top-down
39
+ def top_down_parse(self, sentence: Sequence[str]) -> Optional[List[str]]:
40
+ """Leftmost recursive-descent expansion with backtracking.
41
+
42
+ Returns the resulting stack (``[start]``) on success, ``None`` on
43
+ failure - same contract as the lab sheet, but actually implemented.
44
+ """
45
+ self._tokens = list(sentence)
46
+ self._pos = 0
47
+ if not self._expand(self.start):
48
+ return None
49
+ if self._pos != len(self._tokens):
50
+ return None
51
+ return [self.start]
52
+
53
+ def _expand(self, symbol: str) -> bool:
54
+ if symbol not in self.rules: # terminal
55
+ if self._pos < len(self._tokens) and self._tokens[self._pos] == symbol:
56
+ self._pos += 1
57
+ return True
58
+ return False
59
+
60
+ for rhs in self.rules[symbol]:
61
+ mark = self._pos
62
+ matched = True
63
+ for child in rhs:
64
+ if not self._expand(child):
65
+ matched = False
66
+ break
67
+ if matched:
68
+ return True
69
+ self._pos = mark
70
+ return False
71
+
72
+ # ------------------------------------------------------------------ bottom-up
73
+ def _rule_kinds(self):
74
+ """Split the grammar into lexical, unary and binary rules (CNF view)."""
75
+ lexical: Dict[str, List[str]] = {}
76
+ unary: List[Tuple[str, str]] = []
77
+ binary: List[Tuple[str, str, str]] = []
78
+ for lhs, rhs_list in self.rules.items():
79
+ for rhs in rhs_list:
80
+ if len(rhs) == 1 and rhs[0] not in self.rules:
81
+ lexical.setdefault(rhs[0], []).append(lhs)
82
+ elif len(rhs) == 1:
83
+ unary.append((lhs, rhs[0]))
84
+ elif len(rhs) == 2:
85
+ binary.append((lhs, rhs[0], rhs[1]))
86
+ else:
87
+ raise ValueError(
88
+ f"rule {lhs} -> {' '.join(rhs)} is not supported by the "
89
+ "chart parser; split it into binary rules"
90
+ )
91
+ return lexical, unary, binary
92
+
93
+ def bottom_up_parse(self, sentence: Sequence[str]) -> List[str]:
94
+ """Bottom-up chart (CYK-style) parse over spans of the sentence.
95
+
96
+ Terminals are lifted to their preterminals first, then spans of growing
97
+ length are combined - the standard bottom-up direction. Returns
98
+ ``['S']`` when the whole sentence reduces to the start symbol,
99
+ otherwise the symbols left at the top of the chart.
100
+ """
101
+ tokens = list(sentence)
102
+ if not tokens:
103
+ return []
104
+
105
+ lexical, unary, binary = self._rule_kinds()
106
+
107
+ def close(cell: set) -> set:
108
+ changed = True
109
+ while changed:
110
+ changed = False
111
+ for parent, child in unary:
112
+ if child in cell and parent not in cell:
113
+ cell.add(parent)
114
+ changed = True
115
+ return cell
116
+
117
+ chart: Dict[Tuple[int, int], set] = {}
118
+ for index, token in enumerate(tokens):
119
+ chart[(index, index + 1)] = close(set(lexical.get(token, ())))
120
+
121
+ for length in range(2, len(tokens) + 1):
122
+ for start in range(0, len(tokens) - length + 1):
123
+ end = start + length
124
+ cell: set = set()
125
+ for split in range(start + 1, end):
126
+ left = chart[(start, split)]
127
+ right = chart[(split, end)]
128
+ for parent, first, second in binary:
129
+ if first in left and second in right:
130
+ cell.add(parent)
131
+ chart[(start, end)] = close(cell)
132
+
133
+ top = chart.get((0, len(tokens)), set())
134
+ if self.start in top:
135
+ return [self.start]
136
+ return sorted(top) if top else tokens
137
+
138
+
139
+ def main(
140
+ grammar: Grammar = GRAMMAR,
141
+ sentence: Sequence[str] = SENTENCE,
142
+ ) -> None:
143
+ """Reproduce the lab-sheet output."""
144
+ parser = Parser(grammar)
145
+ print("Top-Down Parse Result:", parser.top_down_parse(sentence))
146
+ print("Bottom-Up Parse Result:", parser.bottom_up_parse(sentence))
147
+
148
+
149
+ if __name__ == "__main__": # pragma: no cover
150
+ main()
@@ -0,0 +1,95 @@
1
+ """Program 5 - add-one smoothed Naive Bayes movie-review classifier.
2
+
3
+ Posteriors are unnormalised class scores (prior x product of likelihoods),
4
+ which is all that is needed to compare classes.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections import defaultdict
10
+ from typing import Dict, Iterable, List, Sequence, Tuple
11
+
12
+ REVIEWS: List[Tuple[str, str]] = [
13
+ ("fun, couple, love, love", "comedy"),
14
+ ("fast, furious, shoot", "action"),
15
+ ("couple, fly, fast, fun", "comedy"),
16
+ ("furious, shoot, shoot, fun", "action"),
17
+ ("fly, fast, shoot, love", "action"),
18
+ ]
19
+
20
+ DOCUMENT = "fast, couple, shoot, fly"
21
+ SEPARATOR = ", "
22
+
23
+
24
+ class NaiveBayes:
25
+ """Multinomial Naive Bayes with add-one (Laplace) smoothing."""
26
+
27
+ def __init__(self, alpha: float = 1.0) -> None:
28
+ self.alpha = alpha
29
+ self.counts: Dict[str, Dict[str, int]] = defaultdict(lambda: defaultdict(int))
30
+ self.class_totals: Dict[str, int] = {}
31
+ self.priors: Dict[str, float] = {}
32
+ self.vocabulary: set = set()
33
+
34
+ def fit(self, rows: Iterable[Tuple[str, str]], separator: str = SEPARATOR) -> "NaiveBayes":
35
+ classes: Dict[str, int] = defaultdict(int)
36
+ for review, label in rows:
37
+ classes[label] += 1
38
+ for word in review.split(separator):
39
+ word = word.strip()
40
+ if not word:
41
+ continue
42
+ self.counts[label][word] += 1
43
+ self.vocabulary.add(word)
44
+
45
+ total_documents = sum(classes.values())
46
+ self.priors = {label: n / total_documents for label, n in classes.items()}
47
+ self.class_totals = {label: sum(words.values()) for label, words in self.counts.items()}
48
+ return self
49
+
50
+ @property
51
+ def vocabulary_size(self) -> int:
52
+ return len(self.vocabulary)
53
+
54
+ def likelihood(self, label: str, word: str) -> float:
55
+ """P(word | label) with add-one smoothing."""
56
+ numerator = self.counts[label][word] + self.alpha
57
+ denominator = self.class_totals.get(label, 0) + self.alpha * self.vocabulary_size
58
+ return numerator / denominator
59
+
60
+ def scores(self, words: Sequence[str]) -> Dict[str, float]:
61
+ """Unnormalised posterior score per class."""
62
+ result: Dict[str, float] = {}
63
+ for label, prior in self.priors.items():
64
+ score = prior
65
+ for word in words:
66
+ score *= self.likelihood(label, word)
67
+ result[label] = score
68
+ return result
69
+
70
+ def predict(self, words: Sequence[str]) -> str:
71
+ scores = self.scores(words)
72
+ return max(scores, key=scores.get) # type: ignore[arg-type]
73
+
74
+
75
+ def main(rows: Sequence[Tuple[str, str]] = REVIEWS, document: str = DOCUMENT) -> None:
76
+ """Reproduce the lab-sheet output (including the capitalised labels)."""
77
+ model = NaiveBayes().fit(rows)
78
+ words = [w for w in document.split(SEPARATOR) if w]
79
+
80
+ comedy = model.priors.get("comedy", 0.0)
81
+ action = model.priors.get("action", 0.0)
82
+ for word in words:
83
+ comedy *= model.likelihood("comedy", word)
84
+ action *= model.likelihood("action", word)
85
+
86
+ print("Posterior for comedy:", comedy)
87
+ print("Posterior for action:", action)
88
+ print(
89
+ "\nThe most likely class for the document is:",
90
+ "Comedy" if comedy > action else "Action",
91
+ )
92
+
93
+
94
+ if __name__ == "__main__": # pragma: no cover
95
+ main()
pdacek/py.typed ADDED
File without changes
@@ -0,0 +1,62 @@
1
+ """Program 1 - text preprocessing: tokenisation, filtration, script
2
+ validation, stop-word removal and stemming.
3
+
4
+ Self-contained listing: paste it into any notebook or run it directly with
5
+ ``python pl01_preprocessing.py``. No download is attempted unless NLTK is
6
+ already installed and the corpus files are missing.
7
+ """
8
+
9
+ import re
10
+
11
+ try:
12
+ import nltk
13
+ from nltk.corpus import stopwords
14
+ from nltk.stem import PorterStemmer
15
+ from nltk.tokenize import word_tokenize
16
+ except ImportError: # keep working without NLTK
17
+ nltk = None
18
+
19
+ text = """Natural Language Processing is a fascinating field of AI.
20
+ It involves processing human languages, like English!"""
21
+
22
+ print("Original Text:\n", text)
23
+
24
+ # 1. Tokenization
25
+ if nltk is not None:
26
+ try:
27
+ tokens = word_tokenize(text)
28
+ except LookupError:
29
+ for resource in ("punkt", "punkt_tab"):
30
+ nltk.download(resource, quiet=True)
31
+ tokens = word_tokenize(text)
32
+ else:
33
+ tokens = re.findall(r"[A-Za-z]+(?:'[A-Za-z]+)?|\d+|[^\sA-Za-z\d]", text)
34
+
35
+ print("\nTokens:\n", tokens)
36
+
37
+ # 2. Filtration
38
+ filtered = [re.sub(r"[^a-zA-Z]", "", w) for w in tokens]
39
+ filtered = [w for w in filtered if w]
40
+ print("\nFiltered Tokens:\n", filtered)
41
+
42
+ # 3. Script Validation
43
+ validated = [w for w in filtered if re.match(r"^[a-zA-Z]+$", w)]
44
+ print("\nValidated Tokens (English Only):\n", validated)
45
+
46
+ # 4. Stop Word Removal
47
+ if nltk is not None:
48
+ try:
49
+ english_stopwords = stopwords.words("english")
50
+ except LookupError:
51
+ nltk.download("stopwords", quiet=True)
52
+ english_stopwords = stopwords.words("english")
53
+ else:
54
+ english_stopwords = ["a", "an", "the", "is", "of", "and", "it", "like", "in", "to"]
55
+
56
+ cleaned = [w for w in validated if w.lower() not in english_stopwords]
57
+ print("\nTokens after Stop Word Removal:\n", cleaned)
58
+
59
+ # 5. Stemming
60
+ stemmer = PorterStemmer() if nltk is not None else None
61
+ stemmed = [stemmer.stem(w) if stemmer else w.lower() for w in cleaned]
62
+ print("\nStemmed Tokens:\n", stemmed)
@@ -0,0 +1,46 @@
1
+ """Program 2 - unigram, bigram and trigram probability estimates.
2
+
3
+ Self-contained listing: ``python pl02_ngrams.py``.
4
+ """
5
+
6
+ import re
7
+ from collections import Counter
8
+
9
+ try:
10
+ from nltk.tokenize import word_tokenize
11
+ except ImportError: # regex fallback below
12
+ word_tokenize = None
13
+
14
+ sentences = [
15
+ "The quick brown fox jumps over the lazy dog",
16
+ "The dog barks at the fox",
17
+ "The fox is quick and the dog is lazy",
18
+ "The quick dog jumps over the lazy fox",
19
+ ]
20
+
21
+
22
+ def tokenize(sentence):
23
+ if word_tokenize is not None:
24
+ try:
25
+ return word_tokenize(sentence.lower())
26
+ except LookupError:
27
+ pass
28
+ return re.findall(r"[a-z]+(?:'[a-z]+)?|[^\sa-z\d]", sentence.lower())
29
+
30
+
31
+ def ngrams(sequence, n):
32
+ return [tuple(sequence[i:i + n]) for i in range(len(sequence) - n + 1)]
33
+
34
+
35
+ # Tokenization
36
+ words = [tokenize(s) for s in sentences]
37
+
38
+ # N-gram probabilities
39
+ for n in [1, 2, 3]:
40
+ count = Counter(g for w in words for g in ngrams(w, n))
41
+ prev = Counter(g for w in words for g in ngrams(w, n - 1)) if n > 1 else None
42
+
43
+ print(f"\n{n}-Gram Probabilities:")
44
+ for g, c in count.items():
45
+ p = c / (sum(count.values()) if n == 1 else prev[g[:-1]])
46
+ print(f"{' '.join(g)}: {p:.4f}")
@@ -0,0 +1,47 @@
1
+ """Program 3 - minimum edit distance with the dynamic-programming table.
2
+
3
+ Self-contained listing: ``python pl03_edit_distance.py``.
4
+ """
5
+
6
+
7
+ def med(a, b):
8
+ """Return (minimum edit distance, DP table)."""
9
+ m, n = len(a), len(b)
10
+ dp = [[0] * (n + 1) for _ in range(m + 1)]
11
+
12
+ for i in range(m + 1):
13
+ dp[i][0] = i
14
+ for j in range(n + 1):
15
+ dp[0][j] = j
16
+
17
+ for i in range(1, m + 1):
18
+ for j in range(1, n + 1):
19
+ if a[i - 1] == b[j - 1]:
20
+ dp[i][j] = dp[i - 1][j - 1]
21
+ else:
22
+ dp[i][j] = 1 + min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1])
23
+ return dp[m][n], dp
24
+
25
+
26
+ def display(dp, a, b):
27
+ print("\nEdit Distance Table:")
28
+ print(" " + " ".join("#" + c for c in " " + b))
29
+ for i, row in enumerate(dp):
30
+ print(("#" if i == 0 else a[i - 1]), *row)
31
+
32
+
33
+ # Test cases
34
+ tests = [
35
+ ("kitten", "sitting"),
36
+ ("flaw", "lawn"),
37
+ ("intention", "execution"),
38
+ ("apple", "aple"),
39
+ ("", "hello"),
40
+ ("hello", ""),
41
+ ]
42
+
43
+ for a, b in tests:
44
+ distance, dp = med(a, b)
45
+ print(f"\nComparing: '{a}' -> '{b}'")
46
+ print("Minimum Edit Distance:", distance)
47
+ display(dp, a, b)
@@ -0,0 +1,95 @@
1
+ """Program 4 - top-down and bottom-up parsing from a context-free grammar.
2
+
3
+ Self-contained listing: ``python pl04_parsing.py``.
4
+ """
5
+
6
+
7
+ class Parser:
8
+ def __init__(self, grammar):
9
+ self.rules = {}
10
+ for lhs, rhs in grammar:
11
+ self.rules.setdefault(lhs, []).append(rhs)
12
+
13
+ # ---- top-down: recursive descent with backtracking ---------------------
14
+ def top_down_parse(self, sentence):
15
+ self.tokens = list(sentence)
16
+ self.pos = 0
17
+ if not self._expand("S"):
18
+ return None
19
+ return ["S"] if self.pos == len(self.tokens) else None
20
+
21
+ def _expand(self, symbol):
22
+ if symbol not in self.rules: # terminal
23
+ if self.pos < len(self.tokens) and self.tokens[self.pos] == symbol:
24
+ self.pos += 1
25
+ return True
26
+ return False
27
+ for rhs in self.rules[symbol]:
28
+ mark = self.pos
29
+ if all(self._expand(child) for child in rhs):
30
+ return True
31
+ self.pos = mark
32
+ return False
33
+
34
+ # ---- bottom-up: chart (CYK-style) parse over spans ---------------------
35
+ def bottom_up_parse(self, sentence, start="S"):
36
+ tokens = list(sentence)
37
+ if not tokens:
38
+ return []
39
+
40
+ lexical, unary, binary = {}, [], []
41
+ for lhs, rhs_list in self.rules.items():
42
+ for rhs in rhs_list:
43
+ if len(rhs) == 1 and rhs[0] not in self.rules:
44
+ lexical.setdefault(rhs[0], []).append(lhs)
45
+ elif len(rhs) == 1:
46
+ unary.append((lhs, rhs[0]))
47
+ elif len(rhs) == 2:
48
+ binary.append((lhs, rhs[0], rhs[1]))
49
+ else:
50
+ raise ValueError(f"rule {lhs} -> {' '.join(rhs)} needs splitting")
51
+
52
+ def close(cell):
53
+ changed = True
54
+ while changed:
55
+ changed = False
56
+ for parent, child in unary:
57
+ if child in cell and parent not in cell:
58
+ cell.add(parent)
59
+ changed = True
60
+ return cell
61
+
62
+ chart = {}
63
+ for i, token in enumerate(tokens):
64
+ chart[(i, i + 1)] = close(set(lexical.get(token, ())))
65
+
66
+ for length in range(2, len(tokens) + 1):
67
+ for s in range(0, len(tokens) - length + 1):
68
+ e = s + length
69
+ cell = set()
70
+ for split in range(s + 1, e):
71
+ left, right = chart[(s, split)], chart[(split, e)]
72
+ for parent, first, second in binary:
73
+ if first in left and second in right:
74
+ cell.add(parent)
75
+ chart[(s, e)] = close(cell)
76
+
77
+ top = chart.get((0, len(tokens)), set())
78
+ return [start] if start in top else (sorted(top) or tokens)
79
+
80
+
81
+ grammar = [
82
+ ("S", ["NP", "VP"]),
83
+ ("NP", ["Det", "N"]),
84
+ ("VP", ["V", "NP"]),
85
+ ("VP", ["V"]),
86
+ ("Det", ["a"]), ("Det", ["the"]),
87
+ ("N", ["cat"]), ("N", ["dog"]),
88
+ ("V", ["chases"]), ("V", ["eats"]),
89
+ ]
90
+
91
+ parser = Parser(grammar)
92
+ sentence = ["the", "cat", "chases", "a", "dog"]
93
+
94
+ print("Top-Down Parse Result:", parser.top_down_parse(sentence))
95
+ print("Bottom-Up Parse Result:", parser.bottom_up_parse(sentence))
@@ -0,0 +1,53 @@
1
+ """Program 5 - add-one smoothed Naive Bayes movie-review classifier.
2
+
3
+ Self-contained listing: ``python pl05_naive_bayes.py``.
4
+ """
5
+
6
+ from collections import defaultdict
7
+
8
+ reviews = [
9
+ ("fun, couple, love, love", "comedy"),
10
+ ("fast, furious, shoot", "action"),
11
+ ("couple, fly, fast, fun", "comedy"),
12
+ ("furious, shoot, shoot, fun", "action"),
13
+ ("fly, fast, shoot, love", "action"),
14
+ ]
15
+
16
+ counts = defaultdict(lambda: defaultdict(int))
17
+ classes = defaultdict(int)
18
+ vocab = set()
19
+
20
+ # Count words
21
+ for review, label in reviews:
22
+ classes[label] += 1
23
+ for word in review.split(", "):
24
+ counts[label][word] += 1
25
+ vocab.add(word)
26
+
27
+ V = len(vocab)
28
+
29
+ # Prior probabilities
30
+ total = sum(classes.values())
31
+ prior = {c: classes[c] / total for c in classes}
32
+
33
+
34
+ # Likelihood with Add-1 smoothing
35
+ def likelihood(c, w):
36
+ total_words = sum(counts[c].values())
37
+ return (counts[c][w] + 1) / (total_words + V)
38
+
39
+
40
+ # Classify new document
41
+ words = "fast, couple, shoot, fly".split(", ")
42
+ comedy = prior["comedy"]
43
+ action = prior["action"]
44
+
45
+ for w in words:
46
+ comedy *= likelihood("comedy", w)
47
+ action *= likelihood("action", w)
48
+
49
+ print("Posterior for comedy:", comedy)
50
+ print("Posterior for action:", action)
51
+
52
+ print("\nThe most likely class for the document is:",
53
+ "Comedy" if comedy > action else "Action")
@@ -0,0 +1,19 @@
1
+ Metadata-Version: 2.5
2
+ Name: pdacek
3
+ Version: 0.1.0
4
+ License: MIT
5
+ License-File: LICENSE
6
+ Requires-Python: >=3.9
7
+ Requires-Dist: nltk>=3.8
8
+ Provides-Extra: dev
9
+ Requires-Dist: build>=1.2; extra == 'dev'
10
+ Requires-Dist: pytest>=8.0; extra == 'dev'
11
+ Requires-Dist: twine>=5.0; extra == 'dev'
12
+ Description-Content-Type: text/markdown
13
+
14
+ import pdacek
15
+ pdacek.p1() # preprocessing pipeline
16
+ pdacek.p2() # n-gram probabilities
17
+ pdacek.p3() # minimum edit distance
18
+ pdacek.p4() # top-down / bottom-up parsing
19
+ pdacek.p5() # Naive Bayes
@@ -0,0 +1,21 @@
1
+ pdacek/__init__.py,sha256=_wbnmUGgNfrfkyUdSd4bmecd6v-Lju8zrsvpcIr4Az4,2794
2
+ pdacek/__main__.py,sha256=YPR_b19rJwqt63H1haKe7o3Uq-xFsfBxl4jbVSOm-vQ,116
3
+ pdacek/cli.py,sha256=ZzzNpzYGBvpPKL0iy1BQgRPqqaTcVZNY_1sjK53v2jQ,2961
4
+ pdacek/compat.py,sha256=C7p5TDJIWD1dbFDzDmCn__KIj3nvCkg2tiHddkDzgqY,4484
5
+ pdacek/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ pdacek/programs/__init__.py,sha256=SW-nyRpS1FFVnhAK00XyOq2eNYwRrd4v0HOoy1G5de8,2676
7
+ pdacek/programs/pl01_preprocessing.py,sha256=VeZSboQRdXyUK5F2BT_jFFVMaw2KwF1yTZw1W4kyxdQ,2435
8
+ pdacek/programs/pl02_ngrams.py,sha256=ziMDozsuZoPWS1cZS6qvPet-oY5eoUqv2n-4ql614oc,2228
9
+ pdacek/programs/pl03_edit_distance.py,sha256=vjO9W88TjYswhC9WbGNYn93vlGjCzw1usoIGgs2hzRI,2766
10
+ pdacek/programs/pl04_parsing.py,sha256=KNsCI2F3hdzkAp4FSrK_4Ryp4xJ4K0_oeW6h04tstU4,5338
11
+ pdacek/programs/pl05_naive_bayes.py,sha256=amWnfqDele05HcTBktsuVlrLBWtANHnslHmlfU92T_o,3375
12
+ pdacek/scripts/pl01_preprocessing.py,sha256=_nZAAqaCzdAOVPYBoHlnZ2dnxYoNxeGHDLQFsKOEXt0,1986
13
+ pdacek/scripts/pl02_ngrams.py,sha256=XWURpSWHQN5DPudpknmGFIEv0jsxmUC5SPrDqlvHr0A,1242
14
+ pdacek/scripts/pl03_edit_distance.py,sha256=6TcDtqNgO3Wvs6F1oTZS3ETlijEP_azbosjDGpfl0D0,1169
15
+ pdacek/scripts/pl04_parsing.py,sha256=bPtmdimyIuJ2qUbcptbS0-FGQbaN-MfWNS-9eQv_EM4,3279
16
+ pdacek/scripts/pl05_naive_bayes.py,sha256=6sv0qnQGn9PEgOiiHPiMx9GLceabakbvRleDXq4qV80,1313
17
+ pdacek-0.1.0.dist-info/METADATA,sha256=lVYrV2xqM_aEonQJ6KZ1baOG1P28RizQYp4QApx5EjE,557
18
+ pdacek-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
19
+ pdacek-0.1.0.dist-info/entry_points.txt,sha256=iQxygOdFfe7fB9ZW3YrAP3fZzjExu0W9dOe0fu4WOl4,43
20
+ pdacek-0.1.0.dist-info/licenses/LICENSE,sha256=IeH_S9lC7iNb5q7sKLWDAI5qviV1-urlfIcisLMXAyw,1071
21
+ pdacek-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pdacek = pdacek.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sohel Havaldar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.