polish-inflection 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.
@@ -0,0 +1,58 @@
1
+ """polish-inflection — odmiana polskich rzeczowników przez przypadki (dane SGJP).
2
+
3
+ Lekka, czysto-Pythonowa biblioteka: kierunek generacji (``odmien``) i analizy
4
+ zwrotnej (``podaj``), oparta o kompaktowe indeksy DAWG zbudowane z danych SGJP.
5
+ """
6
+
7
+ from .const import (
8
+ BIERNIK,
9
+ CELOWNIK,
10
+ DOPEŁNIACZ,
11
+ LICZBY,
12
+ MIANOWNIK,
13
+ MIEJSCOWNIK,
14
+ MNOGA,
15
+ NARZĘDNIK,
16
+ POJEDYNCZA,
17
+ PRZYPADKI,
18
+ TEN_SAM_WYRAZ,
19
+ WOŁACZ,
20
+ )
21
+ from .core import (
22
+ odmien,
23
+ odmien_lub_none,
24
+ odmien_lub_wyraz,
25
+ odmien_warianty,
26
+ podaj,
27
+ )
28
+ from .errors import Analiza, BrakOdmiany
29
+
30
+ __version__ = "0.1.0"
31
+
32
+ __all__ = [
33
+ # funkcje
34
+ "odmien",
35
+ "odmien_lub_none",
36
+ "odmien_lub_wyraz",
37
+ "odmien_warianty",
38
+ "podaj",
39
+ # typy / wyjątki
40
+ "Analiza",
41
+ "BrakOdmiany",
42
+ # stałe przypadków
43
+ "MIANOWNIK",
44
+ "DOPEŁNIACZ",
45
+ "CELOWNIK",
46
+ "BIERNIK",
47
+ "NARZĘDNIK",
48
+ "MIEJSCOWNIK",
49
+ "WOŁACZ",
50
+ "PRZYPADKI",
51
+ # liczby
52
+ "POJEDYNCZA",
53
+ "MNOGA",
54
+ "LICZBY",
55
+ # sentinel
56
+ "TEN_SAM_WYRAZ",
57
+ "__version__",
58
+ ]
@@ -0,0 +1,321 @@
1
+ """Pipeline BUILD (offline): SGJP ``.tab`` -> dwa indeksy ``BytesDAWG``.
2
+
3
+ Uruchamiany u autora / w CI, NIGDY przy ``pip install``. Buduje kompaktowy,
4
+ w pełni wyliczony indeks — runtime niczego nie generuje, tylko wyszukuje.
5
+
6
+ Zależności buildu (extra ``build``): ``dawg2`` (import ``dawg``) do zapisu,
7
+ ``requests`` do ``refresh_sgjp``. Runtime tego nie potrzebuje.
8
+
9
+ Schemat wyjścia — patrz CONTRACT §D / :mod:`polish_inflection.core`.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import gzip
16
+ import hashlib
17
+ import io
18
+ import json
19
+ import re
20
+ import sys
21
+ from collections.abc import Iterable, Iterator
22
+ from pathlib import Path
23
+ from typing import NamedTuple
24
+
25
+ from .const import LICZBY_SET, PRZYPADKI_SET
26
+
27
+ # ── Model rekordu po ekspansji ─────────────────────────────────────────────
28
+
29
+
30
+ class Rekord(NamedTuple):
31
+ forma: str
32
+ lemat: str # goły lemat (bez sufiksu fleksemu)
33
+ liczba: str # "sg"/"pl"
34
+ przypadek: str # "nom".."voc"
35
+ rodzaj: str # "m1","f","n",...
36
+ depr: bool # True dla form deprecjatywnych (tag depr:)
37
+
38
+
39
+ # ── Parsowanie + EKSPANSJA tagu SGJP ───────────────────────────────────────
40
+
41
+
42
+ def goly_lemat(lemat: str) -> str:
43
+ """Utnij sufiks fleksemu/homonimu: ``profesor:Sm1`` -> ``profesor``."""
44
+ return lemat.split(":", 1)[0]
45
+
46
+
47
+ def parsuj_linie(linie: Iterable[str]) -> Iterator[Rekord]:
48
+ """Parsuj i ROZWIŃ linie SGJP ``.tab`` na pojedyncze rekordy.
49
+
50
+ Tag NIE jest 1:1: pola ``liczby`` i ``przypadki`` to kropkowo sklejone
51
+ podzbiory (``subst:sg:nom.acc:m3`` -> dwa rekordy). Pomija nagłówki (``#``),
52
+ tagi inne niż ``subst``/``depr`` oraz wpisy niekompletne/nietypowe.
53
+ """
54
+ for linia in linie:
55
+ linia = linia.rstrip("\n")
56
+ if not linia or linia.startswith("#"):
57
+ continue
58
+ pola = linia.split("\t")
59
+ if len(pola) < 3:
60
+ continue
61
+ forma, lemat, tag = pola[0], pola[1], pola[2]
62
+ if not forma or not tag:
63
+ continue
64
+
65
+ czesci = tag.split(":")
66
+ pos = czesci[0]
67
+ if pos not in ("subst", "depr"):
68
+ continue
69
+ if len(czesci) < 4:
70
+ continue
71
+
72
+ depr = pos == "depr"
73
+ bare = goly_lemat(lemat)
74
+ liczby = czesci[1].split(".")
75
+ przypadki = czesci[2].split(".")
76
+ rodzaj = czesci[3].split(".")[0] # rodzaj bywa kropkowany — bierz główny
77
+
78
+ for liczba in liczby:
79
+ if liczba not in LICZBY_SET:
80
+ continue
81
+ for przypadek in przypadki:
82
+ if przypadek not in PRZYPADKI_SET:
83
+ continue
84
+ yield Rekord(forma, bare, liczba, przypadek, rodzaj, depr)
85
+
86
+
87
+ # ── Budowa indeksów (marisa-trie BytesTrie) ────────────────────────────────
88
+
89
+
90
+ def _pary_dawg(rekordy: Iterable[Rekord]):
91
+ """Zwróć (pary_odmien, pary_podaj) jako listy (klucz:str, wartosc:bytes).
92
+
93
+ odmien: tylko subst (formy główne). podaj: subst + depr.
94
+ Deduplikacja identycznych par.
95
+ """
96
+ odmien_set: set[tuple[str, bytes]] = set()
97
+ podaj_set: set[tuple[str, bytes]] = set()
98
+ for r in rekordy:
99
+ wart_podaj = f"{r.lemat}\t{r.przypadek}\t{r.liczba}\t{r.rodzaj}".encode()
100
+ podaj_set.add((r.forma, wart_podaj))
101
+ if not r.depr:
102
+ klucz = f"{r.lemat}\t{r.przypadek}\t{r.liczba}"
103
+ odmien_set.add((klucz, r.forma.encode("utf-8")))
104
+ return sorted(odmien_set), sorted(podaj_set)
105
+
106
+
107
+ def zbuduj_dawgi(rekordy: Iterable[Rekord]):
108
+ """Zbuduj i zwróć oba ``marisa_trie.BytesTrie`` (odmien, podaj)."""
109
+ import marisa_trie
110
+
111
+ pary_odmien, pary_podaj = _pary_dawg(rekordy)
112
+ return marisa_trie.BytesTrie(pary_odmien), marisa_trie.BytesTrie(pary_podaj)
113
+
114
+
115
+ # ── Czytanie źródła ────────────────────────────────────────────────────────
116
+
117
+
118
+ def _otworz_tab(sciezka: Path) -> Iterator[str]:
119
+ """Otwórz ``.tab`` lub ``.tab.gz`` jako strumień linii (utf-8)."""
120
+ if sciezka.suffix == ".gz":
121
+ with gzip.open(sciezka, "rt", encoding="utf-8") as f:
122
+ yield from f
123
+ else:
124
+ with open(sciezka, encoding="utf-8") as f:
125
+ yield from f
126
+
127
+
128
+ _DICT_ID_RE = re.compile(r"#!DICT-ID\s+(\S+)")
129
+
130
+
131
+ def wersja_sgjp(sciezka: Path) -> str | None:
132
+ """Odczytaj wersję z nagłówka ``#!DICT-ID pl.sgjp.sgjp-YYYY.MM.DD``."""
133
+ for linia in _otworz_tab(sciezka):
134
+ m = _DICT_ID_RE.search(linia)
135
+ if m:
136
+ return m.group(1)
137
+ if not linia.startswith("#"):
138
+ break
139
+ return None
140
+
141
+
142
+ # ── Pełny bieg buildu ──────────────────────────────────────────────────────
143
+
144
+
145
+ def zbuduj_z_tab(sciezka_tab: Path, out_dir: Path, *, data_build: str | None = None) -> dict:
146
+ """Zbuduj ``odmien.marisa``/``podaj.marisa`` + ``BUILD_INFO.json`` w ``out_dir``.
147
+
148
+ Zwraca słownik BUILD_INFO. ``data_build`` przekazuje wywołujący (brak zegara
149
+ w bibliotece); ``None`` -> pole pominięte.
150
+ """
151
+ sciezka_tab = Path(sciezka_tab)
152
+ out_dir = Path(out_dir)
153
+ out_dir.mkdir(parents=True, exist_ok=True)
154
+
155
+ rekordy = list(parsuj_linie(_otworz_tab(sciezka_tab)))
156
+ odmien_trie, podaj_trie = zbuduj_dawgi(rekordy)
157
+
158
+ odmien_path = out_dir / "odmien.marisa"
159
+ podaj_path = out_dir / "podaj.marisa"
160
+ odmien_trie.save(str(odmien_path))
161
+ podaj_trie.save(str(podaj_path))
162
+
163
+ lematy = {r.lemat for r in rekordy}
164
+ formy = {(r.forma, r.lemat, r.liczba, r.przypadek) for r in rekordy}
165
+ info = {
166
+ "wersja_sgjp": wersja_sgjp(sciezka_tab),
167
+ "data_build": data_build,
168
+ "liczba_lematow": len(lematy),
169
+ "liczba_form": len(formy),
170
+ "liczba_rekordow": len(rekordy),
171
+ "rozmiar_odmien_marisa": odmien_path.stat().st_size,
172
+ "rozmiar_podaj_marisa": podaj_path.stat().st_size,
173
+ }
174
+ (out_dir / "BUILD_INFO.json").write_text(
175
+ json.dumps(info, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
176
+ )
177
+ return info
178
+
179
+
180
+ # ── refresh-sgjp (jedyne miejsce sięgające do sieci) ───────────────────────
181
+
182
+ _BAZA_URL = "https://download.sgjp.pl/morfeusz"
183
+ _KATALOG_RE = re.compile(r'href="(\d{8})/"')
184
+
185
+
186
+ def _najnowsza_wersja(sesja) -> str:
187
+ """Znajdź najnowszy katalog dat (``YYYYMMDD``) w indeksie SGJP."""
188
+ r = sesja.get(f"{_BAZA_URL}/", timeout=60)
189
+ r.raise_for_status()
190
+ daty = sorted(set(_KATALOG_RE.findall(r.text)))
191
+ if not daty:
192
+ raise RuntimeError("Nie znaleziono katalogów wersji SGJP.")
193
+ return daty[-1]
194
+
195
+
196
+ def _wytnij_licencje(naglowek: str) -> str | None:
197
+ """Wytnij blok ``#<COPYRIGHT> ... #</COPYRIGHT>`` (verbatim) z nagłówka."""
198
+ linie = []
199
+ zapisuj = False
200
+ for linia in naglowek.splitlines():
201
+ if linia.startswith("#<COPYRIGHT>"):
202
+ zapisuj = True
203
+ continue
204
+ if linia.startswith("#</COPYRIGHT>"):
205
+ break
206
+ if zapisuj:
207
+ linie.append(linia)
208
+ return "\n".join(linie).strip() + "\n" if linie else None
209
+
210
+
211
+ def refresh_sgjp(dane_dir: Path, *, wersja: str | None = None, data_pobrania: str | None = None):
212
+ """Pobierz najnowszy (lub wskazany) SGJP ``.tab.gz``, zwendoruj, zaktualizuj pin.
213
+
214
+ Zapisuje ``sgjp-<wersja>.tab.gz``, ``PIN.json`` i ``LICENSE.sgjp``.
215
+ """
216
+ import requests
217
+
218
+ dane_dir = Path(dane_dir)
219
+ dane_dir.mkdir(parents=True, exist_ok=True)
220
+ sesja = requests.Session()
221
+
222
+ if wersja is None:
223
+ wersja = _najnowsza_wersja(sesja)
224
+ url = f"{_BAZA_URL}/{wersja}/sgjp-{wersja}.tab.gz"
225
+
226
+ print(f"Pobieram {url} ...", file=sys.stderr)
227
+ r = sesja.get(url, timeout=600)
228
+ r.raise_for_status()
229
+ dane = r.content
230
+ sha256 = hashlib.sha256(dane).hexdigest()
231
+
232
+ plik = dane_dir / f"sgjp-{wersja}.tab.gz"
233
+ plik.write_bytes(dane)
234
+
235
+ # Nagłówek do wersji i licencji. UWAGA: treść bloku #<COPYRIGHT>..#</COPYRIGHT>
236
+ # to zwykłe linie BEZ prefiksu '#', więc nie można przerywać na pierwszej
237
+ # linii bez '#'; czytamy dalej dopóki jesteśmy wewnątrz bloku licencji.
238
+ with gzip.open(io.BytesIO(dane), "rt", encoding="utf-8") as f:
239
+ naglowek_linie = []
240
+ wewnatrz_licencji = False
241
+ for linia in f:
242
+ s = linia.rstrip("\n")
243
+ if not wewnatrz_licencji and not s.startswith("#"):
244
+ break
245
+ naglowek_linie.append(linia)
246
+ if s.startswith("#<COPYRIGHT>"):
247
+ wewnatrz_licencji = True
248
+ elif s.startswith("#</COPYRIGHT>"):
249
+ wewnatrz_licencji = False
250
+ naglowek = "".join(naglowek_linie)
251
+ m = _DICT_ID_RE.search(naglowek)
252
+ dict_id = m.group(1) if m else None
253
+
254
+ licencja = _wytnij_licencje(naglowek)
255
+ if licencja:
256
+ (dane_dir / "LICENSE.sgjp").write_text(licencja, encoding="utf-8")
257
+
258
+ pin = {
259
+ "wersja": wersja,
260
+ "dict_id": dict_id,
261
+ "data_pobrania": data_pobrania,
262
+ "sha256": sha256,
263
+ "url_zrodlowy": url,
264
+ "plik": plik.name,
265
+ }
266
+ (dane_dir / "PIN.json").write_text(
267
+ json.dumps(pin, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
268
+ )
269
+ print(f"Zapisano {plik.name} (sha256={sha256[:12]}…), zaktualizowano PIN.json", file=sys.stderr)
270
+ return pin
271
+
272
+
273
+ # ── CLI ────────────────────────────────────────────────────────────────────
274
+
275
+
276
+ def _domyslny_pin_tab(dane_dir: Path) -> Path:
277
+ """Znajdź vendorowany ``.tab.gz`` wg ``PIN.json`` (lub jedyny w katalogu)."""
278
+ pin_path = dane_dir / "PIN.json"
279
+ if pin_path.exists():
280
+ pin = json.loads(pin_path.read_text(encoding="utf-8"))
281
+ nazwa = pin.get("plik")
282
+ if nazwa and (dane_dir / nazwa).exists():
283
+ return dane_dir / nazwa
284
+ kandydaci = sorted(dane_dir.glob("*.tab.gz"))
285
+ if not kandydaci:
286
+ raise SystemExit(f"Brak vendorowanego SGJP w {dane_dir}. Uruchom `refresh-sgjp`.")
287
+ return kandydaci[-1]
288
+
289
+
290
+ def main(argv: list[str] | None = None) -> int:
291
+ repo = Path(__file__).resolve().parents[2]
292
+ parser = argparse.ArgumentParser(prog="polish-inflection-build")
293
+ sub = parser.add_subparsers(dest="cmd", required=True)
294
+
295
+ p_build = sub.add_parser("build", help="Zbuduj indeksy z vendorowanego pinu (bez sieci).")
296
+ p_build.add_argument("--tab", type=Path, default=None, help="Ścieżka do .tab/.tab.gz")
297
+ p_build.add_argument("--out", type=Path, default=repo / "src" / "polish_inflection" / "data")
298
+ p_build.add_argument("--data-build", default=None, help="Data buildu (do BUILD_INFO).")
299
+
300
+ p_refresh = sub.add_parser("refresh-sgjp", help="Pobierz najnowszy SGJP i zaktualizuj pin.")
301
+ p_refresh.add_argument("--wersja", default=None, help="Konkretna wersja YYYYMMDD.")
302
+ p_refresh.add_argument("--data-pobrania", default=None)
303
+ p_refresh.add_argument("--dane-dir", type=Path, default=repo / "data" / "sgjp")
304
+
305
+ args = parser.parse_args(argv)
306
+
307
+ if args.cmd == "build":
308
+ tab = args.tab or _domyslny_pin_tab(repo / "data" / "sgjp")
309
+ info = zbuduj_z_tab(tab, args.out, data_build=args.data_build)
310
+ print(json.dumps(info, ensure_ascii=False, indent=2))
311
+ return 0
312
+
313
+ if args.cmd == "refresh-sgjp":
314
+ refresh_sgjp(args.dane_dir, wersja=args.wersja, data_pobrania=args.data_pobrania)
315
+ return 0
316
+
317
+ return 1
318
+
319
+
320
+ if __name__ == "__main__":
321
+ raise SystemExit(main())
@@ -0,0 +1,37 @@
1
+ """Nazwane stałe przypadków i liczby (mapowane 1:1 na tagi SGJP) oraz sentinel
2
+ ``TEN_SAM_WYRAZ`` dla trybu passthrough w :func:`polish_inflection.core.odmien`.
3
+ """
4
+
5
+ # Przypadki — wartości identyczne z tagami SGJP.
6
+ MIANOWNIK = "nom" # kto? co?
7
+ DOPEŁNIACZ = "gen" # kogo? czego?
8
+ CELOWNIK = "dat" # komu? czemu?
9
+ BIERNIK = "acc" # kogo? co?
10
+ NARZĘDNIK = "inst" # (z) kim? (z) czym?
11
+ MIEJSCOWNIK = "loc" # o kim? o czym?
12
+ WOŁACZ = "voc" # o!
13
+
14
+ # Liczba.
15
+ POJEDYNCZA = "sg"
16
+ MNOGA = "pl"
17
+
18
+ PRZYPADKI = (MIANOWNIK, DOPEŁNIACZ, CELOWNIK, BIERNIK, NARZĘDNIK, MIEJSCOWNIK, WOŁACZ)
19
+ LICZBY = (POJEDYNCZA, MNOGA)
20
+
21
+ # Zbiory do szybkiej walidacji w buildzie/runtime.
22
+ PRZYPADKI_SET = frozenset(PRZYPADKI)
23
+ LICZBY_SET = frozenset(LICZBY)
24
+
25
+
26
+ class _TenSamWyraz:
27
+ """Sentinel: gdy przekazany jako ``default`` do ``odmien`` i brak formy —
28
+ zwróć wejściowy wyraz zamiast rzucać wyjątek."""
29
+
30
+ __slots__ = ()
31
+
32
+ def __repr__(self) -> str:
33
+ return "TEN_SAM_WYRAZ"
34
+
35
+
36
+ #: Wartownik dla ``odmien(..., default=TEN_SAM_WYRAZ)`` — passthrough wejścia.
37
+ TEN_SAM_WYRAZ = _TenSamWyraz()
@@ -0,0 +1,159 @@
1
+ """Runtime API: odmiana (``odmien``) i analiza zwrotna (``podaj``).
2
+
3
+ Czyta gotowe indeksy ``odmien.marisa`` / ``podaj.marisa`` (marisa-trie
4
+ ``BytesTrie``) leniwie i przez ``mmap`` (stała pamięć — import nie ładuje
5
+ słownika do RAM). Schemat kluczy/wartości opisuje CONTRACT §D:
6
+
7
+ odmien.marisa: klucz "lemat\\tprzypadek\\tliczba" -> wartość: forma (utf-8)
8
+ podaj.marisa: klucz "forma" -> wartość: "lemat\\tprzypadek\\tliczba\\trodzaj"
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from importlib.resources import files
14
+ from pathlib import Path
15
+
16
+ import marisa_trie
17
+
18
+ from .const import POJEDYNCZA, TEN_SAM_WYRAZ
19
+ from .errors import Analiza, BrakOdmiany
20
+
21
+ __all__ = [
22
+ "odmien",
23
+ "odmien_lub_none",
24
+ "odmien_lub_wyraz",
25
+ "odmien_warianty",
26
+ "podaj",
27
+ ]
28
+
29
+ # Wewnętrzny wartownik: "default nie podany -> rzuć BrakOdmiany".
30
+ _BRAK = object()
31
+
32
+ # Leniwie ładowane, mmapowane indeksy. Cache modułowy.
33
+ _odmien_dawg = None
34
+ _podaj_dawg = None
35
+
36
+ # Katalog z danymi — domyślnie pakietowy ``polish_inflection/data``.
37
+ # Testy podmieniają go przez ``_ustaw_katalog_danych``.
38
+ _katalog_danych: Path | None = None
39
+
40
+
41
+ def _domyslny_katalog_danych() -> Path:
42
+ return Path(str(files("polish_inflection") / "data"))
43
+
44
+
45
+ def _ustaw_katalog_danych(sciezka) -> None:
46
+ """Wskaż inny katalog z ``odmien.dawg``/``podaj.dawg`` i wyczyść cache.
47
+
48
+ Używane w testach (fixture DAWG budowany do tmp) oraz w integracji.
49
+ ``None`` przywraca katalog pakietowy.
50
+ """
51
+ global _katalog_danych, _odmien_dawg, _podaj_dawg
52
+ _katalog_danych = Path(sciezka) if sciezka is not None else None
53
+ _odmien_dawg = None
54
+ _podaj_dawg = None
55
+
56
+
57
+ def _wczytaj(nazwa: str):
58
+ katalog = _katalog_danych or _domyslny_katalog_danych()
59
+ sciezka = katalog / nazwa
60
+ if not sciezka.exists():
61
+ raise RuntimeError(
62
+ f"Brak zbudowanego indeksu {sciezka}. "
63
+ "Uruchom build: `polish-inflection-build build` (patrz docs/budowanie.md)."
64
+ )
65
+ trie = marisa_trie.BytesTrie()
66
+ trie.mmap(str(sciezka)) # memory-mapped — stała pamięć, bez wczytywania do RAM
67
+ return trie
68
+
69
+
70
+ def _dawg_odmien():
71
+ global _odmien_dawg
72
+ if _odmien_dawg is None:
73
+ _odmien_dawg = _wczytaj("odmien.marisa")
74
+ return _odmien_dawg
75
+
76
+
77
+ def _dawg_podaj():
78
+ global _podaj_dawg
79
+ if _podaj_dawg is None:
80
+ _podaj_dawg = _wczytaj("podaj.marisa")
81
+ return _podaj_dawg
82
+
83
+
84
+ def _formy(wyraz: str, przypadek: str, liczba: str) -> list[str]:
85
+ """Posortowana lista poprawnych form w slocie (może być pusta)."""
86
+ klucz = f"{wyraz}\t{przypadek}\t{liczba}"
87
+ wartosci = _dawg_odmien().get(klucz)
88
+ if not wartosci:
89
+ return []
90
+ return sorted(v.decode("utf-8") for v in wartosci)
91
+
92
+
93
+ def odmien(wyraz: str, przypadek: str, liczba: str = POJEDYNCZA, *, default=_BRAK):
94
+ """Zwróć główną formę ``wyraz`` w danym przypadku i liczbie.
95
+
96
+ Zachowanie przy braku formy (słowo spoza słownika lub liczba nieistniejąca
97
+ dla lematu, np. ``sg`` dla plurale tantum):
98
+
99
+ - ``default`` niepodany -> ``raise BrakOdmiany``
100
+ - ``default is TEN_SAM_WYRAZ`` -> zwróć ``wyraz`` (passthrough)
101
+ - ``default is None`` -> zwróć ``None``
102
+ - ``default = <cokolwiek>`` -> zwróć ``default``
103
+
104
+ Główna forma = pierwsza po sortowaniu bajtowym wartości slotu (deterministyczna).
105
+
106
+ >>> odmien("wydział", "gen")
107
+ 'wydziału'
108
+ >>> odmien("wydział", "gen", "pl")
109
+ 'wydziałów'
110
+ """
111
+ formy = _formy(wyraz, przypadek, liczba)
112
+ if formy:
113
+ return formy[0]
114
+ if default is _BRAK:
115
+ raise BrakOdmiany((wyraz, przypadek, liczba))
116
+ if default is TEN_SAM_WYRAZ:
117
+ return wyraz
118
+ return default
119
+
120
+
121
+ def odmien_lub_none(wyraz: str, przypadek: str, liczba: str = POJEDYNCZA):
122
+ """Jak ``odmien``, ale przy braku formy zwraca ``None`` (nie rzuca)."""
123
+ return odmien(wyraz, przypadek, liczba, default=None)
124
+
125
+
126
+ def odmien_lub_wyraz(wyraz: str, przypadek: str, liczba: str = POJEDYNCZA) -> str:
127
+ """Jak ``odmien``, ale przy braku formy zwraca wejściowy ``wyraz`` (passthrough)."""
128
+ return odmien(wyraz, przypadek, liczba, default=TEN_SAM_WYRAZ)
129
+
130
+
131
+ def odmien_warianty(wyraz: str, przypadek: str, liczba: str = POJEDYNCZA) -> list[str]:
132
+ """Wszystkie poprawne formy w slocie (oboczności), posortowane. ``[]`` gdy brak."""
133
+ return _formy(wyraz, przypadek, liczba)
134
+
135
+
136
+ def podaj(wyraz: str, liczba: str | None = None) -> list[Analiza]:
137
+ """Kierunek zwrotny: forma -> lista analiz.
138
+
139
+ Zwraca LISTĘ, bo polszczyzna ma synkretyzm (jedna forma = wiele przypadków)
140
+ i homografię (jedna forma = wiele lematów). Uwzględnia też formy
141
+ deprecjatywne (``depr``). Opcjonalny ``liczba`` ("sg"/"pl") zawęża wynik.
142
+ Nieznana forma -> ``[]``.
143
+
144
+ >>> podaj("jednostki") # doctest: +SKIP
145
+ [Analiza('jednostka','gen','sg','f'), Analiza('jednostka','nom','pl','f'), ...]
146
+ """
147
+ wartosci = _dawg_podaj().get(wyraz)
148
+ if not wartosci:
149
+ return []
150
+ analizy: set[Analiza] = set()
151
+ for w in wartosci:
152
+ pola = w.decode("utf-8").split("\t")
153
+ if len(pola) != 4:
154
+ continue
155
+ lemat, przypadek, lb, rodzaj = pola
156
+ if liczba is not None and lb != liczba:
157
+ continue
158
+ analizy.add(Analiza(lemat, przypadek, lb, rodzaj))
159
+ return sorted(analizy, key=lambda a: (a.liczba, a.przypadek, a.lemat, a.rodzaj))
File without changes
@@ -0,0 +1,9 @@
1
+ {
2
+ "wersja_sgjp": "pl.sgjp.sgjp-2026.06.29",
3
+ "data_build": "2026-07-02",
4
+ "liczba_lematow": 223748,
5
+ "liczba_form": 3666104,
6
+ "liczba_rekordow": 3864426,
7
+ "rozmiar_odmien_marisa": 24431696,
8
+ "rozmiar_podaj_marisa": 25000464
9
+ }
Binary file
Binary file
@@ -0,0 +1,21 @@
1
+ """Typy wyników i wyjątki publicznego API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import NamedTuple
6
+
7
+
8
+ class Analiza(NamedTuple):
9
+ """Pojedyncza analiza morfologiczna formy (kierunek zwrotny ``podaj``)."""
10
+
11
+ lemat: str
12
+ przypadek: str # jedna ze stałych PRZYPADKI: "nom".."voc"
13
+ liczba: str # "sg" / "pl"
14
+ rodzaj: str # tag rodzaju SGJP: "m1","m2","m3","f","n","ncol",...
15
+
16
+
17
+ class BrakOdmiany(KeyError):
18
+ """Brak formy dla (wyraz, przypadek, liczba) i nie podano ``default``.
19
+
20
+ Dziedziczy po ``KeyError`` — bo to w istocie brak klucza w słowniku form.
21
+ """
@@ -0,0 +1,291 @@
1
+ Metadata-Version: 2.4
2
+ Name: polish-inflection
3
+ Version: 0.1.0
4
+ Summary: Lekka odmiana polskich rzeczowników przez przypadki (dane SGJP, indeks marisa-trie, mmap).
5
+ Project-URL: Homepage, https://github.com/mpasternak/polish-inflection
6
+ Project-URL: Repository, https://github.com/mpasternak/polish-inflection
7
+ Author-email: Michał Pasternak <m@iplweb.pl>
8
+ License: BSD-2-Clause
9
+ License-File: LICENSE
10
+ License-File: NOTICE.md
11
+ Keywords: declension,inflection,morphology,nlp,odmiana,polish,przypadki,sgjp
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: BSD License
15
+ Classifier: Natural Language :: Polish
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Text Processing :: Linguistic
23
+ Requires-Python: >=3.9
24
+ Requires-Dist: marisa-trie>=1.0
25
+ Provides-Extra: build
26
+ Requires-Dist: requests>=2.28; extra == 'build'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # polish-inflection
30
+
31
+ [![CI](https://github.com/iplweb/polish-inflection/actions/workflows/ci.yml/badge.svg)](https://github.com/iplweb/polish-inflection/actions/workflows/ci.yml)
32
+ [![License: BSD-2-Clause](https://img.shields.io/badge/License-BSD--2--Clause-blue.svg)](LICENSE)
33
+
34
+ Lekka, dane-only odmiana polskich **rzeczowników** przez przypadki.
35
+ Dane pochodzą ze [Słownika gramatycznego języka polskiego (SGJP)](https://morfeusz.sgjp.pl/),
36
+ wyszukiwanie oparte jest o kompaktowy indeks `marisa-trie` (mmap, ~1 µs/lookup).
37
+ Instalacja bez kompilatora (gotowe binarne wheele), zero Django, jedna
38
+ zależność runtime.
39
+
40
+ ---
41
+
42
+ ## 🇵🇱 Wersja polska
43
+
44
+ ### Cel
45
+
46
+ Chcesz z wyrazu `wydział` dostać `wydziału` (dopełniacz), `wydziałowi`
47
+ (celownik), `wydziałów` (dopełniacz liczby mnogiej) — programowo, bez trzymania
48
+ form ręcznie w tabeli. `polish-inflection` odmienia rzeczowniki przez
49
+ **7 przypadków × 2 liczby** w obie strony:
50
+
51
+ - generacja: `lemat + przypadek + liczba → forma` (`odmien`);
52
+ - analiza (kierunek zwrotny): `forma → [analizy]` (`podaj`).
53
+
54
+ ### Dlaczego — luka w ekosystemie
55
+
56
+ Nie istniało lekkie, dane-only, przyjazne wdrożeniu rozwiązanie do
57
+ odmiany polskich rzeczowników przez przypadki:
58
+
59
+ - **gettext / formy mnogie** rozwiązują *inny* problem — wybór wariantu wg
60
+ **liczby** (`nplurals`), a nie odmianę przez **przypadki**. gettext nie zna
61
+ pojęcia przypadka i nie potrafi odmienić rzeczownika.
62
+ - **Morfeusz 2** to kanoniczny analizator i generator morfologii polskiej, ale
63
+ natywny silnik C++ + bindingi SWIG + kompilacja słownika rzędu dziesiątek MB.
64
+ Armata na muchę i ból wdrożeniowy w kontenerze. Tu nie ma silnika, SWIG-a ani
65
+ kompilacji słownika — tylko dane i `pip install`.
66
+ - **pymorphy2 / pymorphy3** obsługują tylko rosyjski i ukraiński — nie polski.
67
+ - **inflection / inflect / pyinflect** — tylko angielski.
68
+
69
+ Kluczowa idea: bierzemy **dane** SGJP (trójki `forma / lemat / tag`), a odrzucamy
70
+ **silnik**. Zbiór form jest zamknięty i w pełni wyliczony — nic nie generujemy
71
+ w locie, tylko indeksujemy i wyszukujemy.
72
+
73
+ ### Instalacja
74
+
75
+ ```bash
76
+ pip install polish-inflection
77
+ ```
78
+
79
+ Jedyna zależność runtime to `marisa-trie` — kompilowane rozszerzenie C++, które
80
+ dostarcza gotowe binarne wheele (Linux/macOS/Windows, cp39–cp313), więc
81
+ `pip install` nie potrzebuje kompilatora ani narzędzi build. Pakiet wiezie gotowe
82
+ indeksy `.marisa` w wheelu — bez SGJP. Czytnik używa **mmap**: import nie ładuje
83
+ słownika do RAM, a pojedynczy lookup stronicuje tylko O(długość słowa) węzłów
84
+ (~1 µs, mierzony narzut RSS ~1,5 MB zamiast ~23 MB pełnego wczytania).
85
+
86
+ ### Przykłady — `odmien`
87
+
88
+ ```python
89
+ from polish_inflection import odmien, DOPEŁNIACZ, MNOGA
90
+
91
+ odmien("wydział", DOPEŁNIACZ) # -> "wydziału"
92
+ odmien("wydział", DOPEŁNIACZ, MNOGA) # -> "wydziałów"
93
+ ```
94
+
95
+ Zachowanie przy słowie spoza słownika jest sterowane parametrem `default`:
96
+
97
+ ```python
98
+ from polish_inflection import (
99
+ odmien, odmien_lub_none, odmien_lub_wyraz, BrakOdmiany, DOPEŁNIACZ,
100
+ )
101
+
102
+ odmien("wydział", DOPEŁNIACZ) # "wydziału"
103
+ odmien("qwerty", DOPEŁNIACZ) # raise BrakOdmiany
104
+ odmien_lub_none("qwerty", DOPEŁNIACZ) # None
105
+ odmien_lub_wyraz("qwerty", DOPEŁNIACZ) # "qwerty" (passthrough wejścia)
106
+ odmien("qwerty", DOPEŁNIACZ, default="—") # "—" (dowolny fallback)
107
+ ```
108
+
109
+ Oboczności (kilka poprawnych form w jednym slocie) zwraca `odmien_warianty`:
110
+
111
+ ```python
112
+ from polish_inflection import odmien_warianty, MIEJSCOWNIK
113
+ odmien_warianty("pokój", MIEJSCOWNIK) # -> lista wszystkich poprawnych form
114
+ ```
115
+
116
+ ### Przykłady — `podaj` (kierunek zwrotny)
117
+
118
+ Polszczyzna ma synkretyzm (jedna forma = wiele przypadków) i homografię (jedna
119
+ forma = wiele lematów), więc `podaj` zwraca **listę** analiz:
120
+
121
+ ```python
122
+ from polish_inflection import podaj
123
+
124
+ podaj("jednostki")
125
+ # [Analiza(lemat='jednostka', przypadek='gen', liczba='sg', rodzaj='f'),
126
+ # Analiza(lemat='jednostka', przypadek='nom', liczba='pl', rodzaj='f'),
127
+ # Analiza(lemat='jednostka', przypadek='acc', liczba='pl', rodzaj='f'),
128
+ # Analiza(lemat='jednostka', przypadek='voc', liczba='pl', rodzaj='f')]
129
+
130
+ podaj("jednostki", liczba="pl") # zawężenie do liczby mnogiej
131
+ ```
132
+
133
+ `Analiza` to lekki `NamedTuple`: `(lemat, przypadek, liczba, rodzaj)`.
134
+
135
+ ### Stałe
136
+
137
+ Przypadki: `MIANOWNIK`, `DOPEŁNIACZ`, `CELOWNIK`, `BIERNIK`, `NARZĘDNIK`,
138
+ `MIEJSCOWNIK`, `WOŁACZ`. Liczby: `POJEDYNCZA`, `MNOGA` (domyślnie `POJEDYNCZA`).
139
+ Sentinel `TEN_SAM_WYRAZ` służy jako `default` zwracający wejściowy wyraz.
140
+
141
+ ### Źródło danych i atrybucja
142
+
143
+ Dane fleksyjne pochodzą ze **Słownika gramatycznego języka polskiego (SGJP)**,
144
+ w wersji przypiętej i zwendorowanej w repozytorium (patrz `data/sgjp/`).
145
+ Zbudowane indeksy pokrywają 223 748 lematów / ~3,86 mln rekordów form i ważą
146
+ łącznie ~49 MB (`odmien.marisa` ≈ 24 MB, `podaj.marisa` ≈ 25 MB).
147
+
148
+ > Copyright © 2007–2026 Marcin Woliński, Zbigniew Bronk, Włodzimierz
149
+ > Gruszczyński, Witold Kieraś, Zygmunt Saloni, Danuta Skowrońska, Robert Wołosz.
150
+
151
+ SGJP jest udostępniany na licencji 2-clause BSD. Strona licencyjna:
152
+ <https://morfeusz.sgjp.pl/doc/license/en>. Pełny tekst i szczegóły atrybucji —
153
+ patrz [`NOTICE.md`](NOTICE.md) oraz `data/sgjp/LICENSE.sgjp`.
154
+
155
+ ### Licencja
156
+
157
+ Dwie warstwy, jawnie rozdzielone:
158
+
159
+ - **Kod pakietu** — BSD-2-Clause, © Michał Pasternak (plik [`LICENSE`](LICENSE)).
160
+ - **Dane SGJP** — BSD-2-Clause, © autorzy wymienieni wyżej. Redystrybucja jest
161
+ dozwolona pod warunkiem zachowania noty copyright, tekstu licencji i atrybucji
162
+ (szczegóły w [`NOTICE.md`](NOTICE.md)).
163
+
164
+ ### Ograniczenia (v1)
165
+
166
+ - Tylko **rzeczowniki**. Bez czasowników, przymiotników, liczebników.
167
+ - Bez guessera słów spoza słownika i bez analizy biegnącego tekstu
168
+ (segmentacji/tokenizacji) — świadome YAGNI.
169
+ - Zakres: 7 przypadków × 2 liczby, oba kierunki (`odmien` / `podaj`).
170
+
171
+ ---
172
+
173
+ ## 🇬🇧 English version
174
+
175
+ ### Purpose
176
+
177
+ Turn `wydział` into `wydziału` (genitive), `wydziałowi` (dative), `wydziałów`
178
+ (genitive plural) — programmatically, without keeping inflected forms by hand.
179
+ `polish-inflection` declines Polish **nouns** across **7 cases × 2 numbers** in
180
+ both directions:
181
+
182
+ - generation: `lemma + case + number → form` (`odmien`);
183
+ - analysis (reverse): `form → [analyses]` (`podaj`).
184
+
185
+ ### Why — a gap in the ecosystem
186
+
187
+ There was no lightweight, data-only, deployment-friendly way to decline Polish
188
+ nouns by case:
189
+
190
+ - **gettext / plural forms** solve a *different* problem — picking a variant by
191
+ **number** (`nplurals`), not declension by **case**. gettext has no concept of
192
+ grammatical case and cannot decline a noun.
193
+ - **Morfeusz 2** is the canonical Polish morphological analyzer and generator,
194
+ but ships a native C++ engine + SWIG bindings + a compiled dictionary of tens
195
+ of MB. Overkill and a container-deployment headache. Here there is no engine,
196
+ no SWIG and no dictionary compilation — just data and `pip install`.
197
+ - **pymorphy2 / pymorphy3** cover only Russian and Ukrainian — not Polish.
198
+ - **inflection / inflect / pyinflect** — English only.
199
+
200
+ Core idea: take the SGJP **data** (`form / lemma / tag` triples), drop the
201
+ **engine**. The set of forms is closed and fully enumerated — we generate
202
+ nothing at runtime, we only index and look up.
203
+
204
+ ### Installation
205
+
206
+ ```bash
207
+ pip install polish-inflection
208
+ ```
209
+
210
+ The only runtime dependency is `marisa-trie` — a compiled C++ extension that
211
+ ships prebuilt binary wheels (Linux/macOS/Windows, cp39–cp313), so `pip install`
212
+ needs no compiler and no build tooling. The wheel ships prebuilt `.marisa`
213
+ indices — no SGJP required. The reader uses **mmap**: import does not load the
214
+ dictionary into RAM, and a single lookup pages in only O(word length) nodes
215
+ (~1 µs, measured RSS overhead ~1.5 MB instead of ~23 MB for a full load). The
216
+ indices cover 223,748 lemmas / ~3.86M form records and weigh ~49 MB total
217
+ (`odmien.marisa` ≈ 24 MB, `podaj.marisa` ≈ 25 MB).
218
+
219
+ ### Examples — `odmien`
220
+
221
+ ```python
222
+ from polish_inflection import odmien, DOPEŁNIACZ, MNOGA
223
+
224
+ odmien("wydział", DOPEŁNIACZ) # -> "wydziału"
225
+ odmien("wydział", DOPEŁNIACZ, MNOGA) # -> "wydziałów"
226
+ ```
227
+
228
+ Out-of-dictionary behaviour is controlled by `default`:
229
+
230
+ ```python
231
+ from polish_inflection import (
232
+ odmien, odmien_lub_none, odmien_lub_wyraz, BrakOdmiany, DOPEŁNIACZ,
233
+ )
234
+
235
+ odmien("wydział", DOPEŁNIACZ) # "wydziału"
236
+ odmien("qwerty", DOPEŁNIACZ) # raises BrakOdmiany
237
+ odmien_lub_none("qwerty", DOPEŁNIACZ) # None
238
+ odmien_lub_wyraz("qwerty", DOPEŁNIACZ) # "qwerty" (passthrough)
239
+ odmien("qwerty", DOPEŁNIACZ, default="—") # "—" (any fallback)
240
+ ```
241
+
242
+ `odmien_warianty` returns all valid forms in a slot (variants).
243
+
244
+ ### Examples — `podaj` (reverse direction)
245
+
246
+ Polish has syncretism (one form = many cases) and homography (one form = many
247
+ lemmas), so `podaj` returns a **list** of analyses:
248
+
249
+ ```python
250
+ from polish_inflection import podaj
251
+
252
+ podaj("jednostki")
253
+ # [Analiza(lemat='jednostka', przypadek='gen', liczba='sg', rodzaj='f'),
254
+ # Analiza(lemat='jednostka', przypadek='nom', liczba='pl', rodzaj='f'),
255
+ # Analiza(lemat='jednostka', przypadek='acc', liczba='pl', rodzaj='f'),
256
+ # Analiza(lemat='jednostka', przypadek='voc', liczba='pl', rodzaj='f')]
257
+
258
+ podaj("jednostki", liczba="pl") # narrow to plural
259
+ ```
260
+
261
+ `Analiza` is a lightweight `NamedTuple`: `(lemat, przypadek, liczba, rodzaj)`.
262
+ Constant names stay Polish (`MIANOWNIK`, `DOPEŁNIACZ`, …, `POJEDYNCZA`, `MNOGA`)
263
+ and map to SGJP tags (`nom`, `gen`, …, `sg`, `pl`).
264
+
265
+ ### Data source and attribution
266
+
267
+ The inflectional data comes from the **Grammatical Dictionary of Polish (SGJP)**,
268
+ pinned and vendored in this repository (see `data/sgjp/`).
269
+
270
+ > Copyright © 2007–2026 Marcin Woliński, Zbigniew Bronk, Włodzimierz
271
+ > Gruszczyński, Witold Kieraś, Zygmunt Saloni, Danuta Skowrońska, Robert Wołosz.
272
+
273
+ SGJP is distributed under the 2-clause BSD license. License page:
274
+ <https://morfeusz.sgjp.pl/doc/license/en>. Full text and attribution details are
275
+ in [`NOTICE.md`](NOTICE.md) and `data/sgjp/LICENSE.sgjp`.
276
+
277
+ ### License
278
+
279
+ Two clearly separated layers:
280
+
281
+ - **Package code** — BSD-2-Clause, © Michał Pasternak (see [`LICENSE`](LICENSE)).
282
+ - **SGJP data** — BSD-2-Clause, © the authors listed above. Redistribution is
283
+ permitted provided the copyright notice, license text and attribution are
284
+ retained (details in [`NOTICE.md`](NOTICE.md)).
285
+
286
+ ### Limitations (v1)
287
+
288
+ - **Nouns only.** No verbs, adjectives or numerals.
289
+ - No out-of-dictionary guesser and no running-text analysis
290
+ (segmentation/tokenization) — deliberate YAGNI.
291
+ - Scope: 7 cases × 2 numbers, both directions (`odmien` / `podaj`).
@@ -0,0 +1,15 @@
1
+ polish_inflection/__init__.py,sha256=FLtEN1xpgwMCVbjXMjWeCNZXn_ouzyqEdLihPuJuCDM,1079
2
+ polish_inflection/build.py,sha256=caKWROxu9byJ0wnWV5wYIHK870ZtASGzO7uoYHFjkiw,11783
3
+ polish_inflection/const.py,sha256=REPN0ZU3cbwu3Xk2ObvrUapIvX6AcA79ndukd8bwx5g,1089
4
+ polish_inflection/core.py,sha256=kxkc0Y5fGorS8LlMIGQTxEzCyYO7eM5bXead0XNPXNw,5387
5
+ polish_inflection/errors.py,sha256=dExGr0Is-ukrwyP1NYMEfsxWg_UPir2aaXXPKeA_zxc,601
6
+ polish_inflection/data/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ polish_inflection/data/BUILD_INFO.json,sha256=ZVhYVvZAHHdbSjqn3V-P8qJEGhldC48aIw-GV05lI1A,234
8
+ polish_inflection/data/odmien.marisa,sha256=KazL74jUzrjS22bXTkem0A1rkamFkmaB0sCvqah_tWU,24431696
9
+ polish_inflection/data/podaj.marisa,sha256=fLa2NQ6NaevYLgmObDqUus-WqPaPkQguFo1omEeYOiU,25000464
10
+ polish_inflection-0.1.0.dist-info/METADATA,sha256=nKUfcsoAoOgjA7p8gRStJCSMRPWOXryOpee7ioguwlg,11956
11
+ polish_inflection-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
12
+ polish_inflection-0.1.0.dist-info/entry_points.txt,sha256=NYsrpqKqDhRjqpz2GHAAEvP_BGtedwvyCkSZxh773ws,73
13
+ polish_inflection-0.1.0.dist-info/licenses/LICENSE,sha256=wC4FTktT4Qi-N1mkI5oaLMNCXXqFZi1hdfQMUdjan5Q,1306
14
+ polish_inflection-0.1.0.dist-info/licenses/NOTICE.md,sha256=pf3EkpRYcvrh5mZXAsPh9jBCIVGoDuw8BkoSVK673es,3050
15
+ polish_inflection-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ polish-inflection-build = polish_inflection.build:main
@@ -0,0 +1,24 @@
1
+ BSD 2-Clause License
2
+
3
+ Copyright (c) 2026, Michał Pasternak
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
19
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
21
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
22
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
23
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,68 @@
1
+ # NOTICE — licencje i atrybucja
2
+
3
+ Pakiet `polish-inflection` łączy dwie warstwy o **osobnych** właścicielach praw
4
+ autorskich i osobnych (choć identycznych co do treści) licencjach. Obie są
5
+ 2-clause BSD; obie wymagają zachowania noty copyright i tekstu licencji przy
6
+ redystrybucji.
7
+
8
+ ---
9
+
10
+ ## 1. Kod pakietu
11
+
12
+ Copyright © 2026 Michał Pasternak.
13
+ Licencja: **BSD-2-Clause**. Pełny tekst: plik [`LICENSE`](LICENSE) w tym repo.
14
+
15
+ Obejmuje kod źródłowy w `src/polish_inflection/` (runtime, build, CLI), testy
16
+ oraz dokumentację. **Nie** obejmuje danych fleksyjnych ani zbudowanych z nich
17
+ indeksów `.marisa` — te podlegają warstwie 2.
18
+
19
+ ---
20
+
21
+ ## 2. Dane SGJP (Słownik gramatyczny języka polskiego)
22
+
23
+ Indeksy `.marisa` w `src/polish_inflection/data/` są zbudowane z danych
24
+ fleksyjnych SGJP i stanowią ich utwór zależny. Podlegają licencji i atrybucji
25
+ SGJP.
26
+
27
+ **Copyright © 2007–2026 Marcin Woliński, Zbigniew Bronk, Włodzimierz
28
+ Gruszczyński, Witold Kieraś, Zygmunt Saloni, Danuta Skowrońska, Robert Wołosz.**
29
+
30
+ Licencja: **BSD-2-Clause**.
31
+ Strona licencyjna: <https://morfeusz.sgjp.pl/doc/license/en>.
32
+
33
+ Autorytatywny, verbatim tekst licencji jadący z konkretnym wydaniem SGJP
34
+ znajduje się w pliku **`data/sgjp/LICENSE.sgjp`** (wyodrębniony z nagłówka
35
+ `#<COPYRIGHT>` pobranego pliku `.tab.gz` przez komendę `refresh-sgjp`). W razie
36
+ jakiejkolwiek rozbieżności rozstrzygający jest tamten plik, nie ten dokument.
37
+
38
+ ### Warunki 2-clause BSD (cytat)
39
+
40
+ ```
41
+ Redistribution and use in source and binary forms, with or without
42
+ modification, are permitted provided that the following conditions are met:
43
+
44
+ 1. Redistributions of source code must retain the above copyright notice, this
45
+ list of conditions and the following disclaimer.
46
+
47
+ 2. Redistributions in binary form must reproduce the above copyright notice,
48
+ this list of conditions and the following disclaimer in the documentation
49
+ and/or other materials provided with the distribution.
50
+
51
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
52
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
53
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
54
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
55
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
56
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
57
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
58
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
59
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
60
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
61
+ ```
62
+
63
+ ### Konsekwencje redystrybucji
64
+
65
+ Redystrybuując `polish-inflection` (w tym wheel/sdist zawierające `.marisa`),
66
+ zachowujesz notę copyright SGJP, powyższe warunki i atrybucję — co realizuje ten
67
+ plik `NOTICE.md` (dołączany do dystrybucji) wraz z verbatim `data/sgjp/LICENSE.sgjp`
68
+ podróżującym z danymi w repozytorium.