zxcvbn-pl 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ .DS_Store
@@ -0,0 +1,7 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 — 2026-10-24
4
+
5
+ - First release: `pl_common_words`, `pl_first_names` and `pl_last_names`
6
+ dictionaries built from `@zxcvbn-ts/language-pl` 4.1.1, with
7
+ diacritics-free variants; `zxcvbn_pl.register()`.
@@ -0,0 +1,24 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Michał Pasternak, IPLweb
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.
22
+
23
+ The bundled dictionaries in src/zxcvbn_pl/data are derived from third-party
24
+ data under their own licenses (ODC-BY, CC0 1.0) — see NOTICE.md.
@@ -0,0 +1,37 @@
1
+ # Data sources and licenses
2
+
3
+ The code of zxcvbn-pl is licensed under the MIT License (see `LICENSE`).
4
+ The dictionaries in `src/zxcvbn_pl/data/*.marisa` are derived from the
5
+ [`@zxcvbn-ts/language-pl`](https://github.com/zxcvbn-ts/zxcvbn/tree/master/packages/languages/pl)
6
+ npm package, version 4.1.1 (MIT), and carry the licenses of their sources:
7
+
8
+ ## `pl_common_words.marisa`
9
+
10
+ Derived from OpenSubtitles via OPUS (`commonWords.json` in
11
+ `@zxcvbn-ts/language-pl`).
12
+
13
+ - Source dataset: OpenSubtitles 2024, https://opus.nlpl.eu/OpenSubtitles.php
14
+ - Provider: Helsinki-NLP / OPUS
15
+ - License: **ODC-BY** (Open Data Commons Attribution License),
16
+ https://opendatacommons.org/licenses/by/
17
+ - Original data is attributed to subtitle contributors and rights holders
18
+ (http://www.opensubtitles.org/).
19
+
20
+ This package contains a processed derivative: tokenized text, frequency
21
+ aggregation, normalization and filtering (by zxcvbn-ts), then lowercasing,
22
+ dropping entries shorter than 3 characters or containing non-letters, adding
23
+ diacritics-free variants and converting to a marisa-trie index (by
24
+ zxcvbn-pl). Any redistribution must retain this notice.
25
+
26
+ ## `pl_first_names.marisa`, `pl_last_names.marisa`
27
+
28
+ Derived from the lists of first names and last names in the Polish PESEL
29
+ registry, published by the Ministry of Digital Affairs on dane.gov.pl:
30
+
31
+ - Lista imion występujących w rejestrze PESEL,
32
+ https://dane.gov.pl/pl/dataset/1667 — **CC0 1.0**
33
+ - Nazwiska występujące w rejestrze PESEL,
34
+ https://dane.gov.pl/pl/dataset/1681 — **CC0 1.0**
35
+
36
+ zxcvbn-ts keeps names with at least 200 occurrences; zxcvbn-pl merges the
37
+ male and female lists, lowercases them and adds diacritics-free variants.
@@ -0,0 +1,152 @@
1
+ Metadata-Version: 2.5
2
+ Name: zxcvbn-pl
3
+ Version: 0.1.0
4
+ Summary: Polish dictionaries (common words, first and last names) for zxcvbn password strength estimation.
5
+ Project-URL: Homepage, https://github.com/iplweb/zxcvbn-pl
6
+ Project-URL: Repository, https://github.com/iplweb/zxcvbn-pl
7
+ Project-URL: Changelog, https://github.com/iplweb/zxcvbn-pl/blob/main/CHANGELOG.md
8
+ Author-email: Michał Pasternak <m@iplweb.pl>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ License-File: NOTICE.md
12
+ Keywords: dictionary,hasło,password,password-strength,polish,security,zxcvbn
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Natural Language :: Polish
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
24
+ Classifier: Topic :: Security
25
+ Requires-Python: >=3.9
26
+ Requires-Dist: marisa-trie>=1.0
27
+ Requires-Dist: zxcvbn>=4.4
28
+ Description-Content-Type: text/markdown
29
+
30
+ # zxcvbn-pl
31
+
32
+ [![PyPI](https://img.shields.io/pypi/v/zxcvbn-pl.svg)](https://pypi.org/project/zxcvbn-pl/)
33
+ [![CI](https://github.com/iplweb/zxcvbn-pl/actions/workflows/ci.yml/badge.svg)](https://github.com/iplweb/zxcvbn-pl/actions/workflows/ci.yml)
34
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
35
+
36
+ Polskie słowniki dla [zxcvbn](https://github.com/dwolfhub/zxcvbn-python)
37
+ (port w Pythonie) — pospolite słowa, imiona i nazwiska.
38
+
39
+ *Polish dictionaries for the Python port of zxcvbn — common words, first
40
+ names and last names. [English below.](#english)*
41
+
42
+ ---
43
+
44
+ ## 🇵🇱 Po polsku
45
+
46
+ ### Po co
47
+
48
+ zxcvbn szacuje, ile prób potrzeba do odgadnięcia hasła, rozpoznając w nim
49
+ słowa ze słowników, powtórzenia, sekwencje, układy klawiatury i daty. Ma
50
+ jednak **tylko angielskie słowniki** — polskie słowo to dla niego losowe
51
+ litery. Przez to hasła łatwe do zgadnięcia dla każdego, kto ma polską listę
52
+ słów, dostają najwyższą ocenę:
53
+
54
+ | Hasło | zxcvbn | zxcvbn + zxcvbn-pl |
55
+ |---|---|---|
56
+ | `słoneczko123456` | 4 | 2 |
57
+ | `pomidorowa123456` | 3 | 2 |
58
+ | `Katarzyna1985!` | 3 | 2 |
59
+ | `agnieszka.nowak1` | 4 | 3 |
60
+ | `Wojciechowski77` | 2 | 1 |
61
+ | `ptaszek-na-drzewie` | 4 | 4 |
62
+ | `zielonaherbatazcytryna` | 4 | 4 |
63
+
64
+ Frazy z kilku słów nadal są oceniane wysoko — tak jak zaleca NIST
65
+ SP 800-63B. Spada ocena haseł z jednego pospolitego słowa, imienia lub
66
+ nazwiska z dopiskiem. Efekt jest umiarkowany: słowa spoza ok. 50 tys.
67
+ najczęstszych (np. `kaczuszka2000!!`) nadal są dla zxcvbn losowymi literami.
68
+
69
+ Port zxcvbn w JavaScripcie (zxcvbn-ts) ma pakiety językowe; port w Pythonie
70
+ — nie. Ten pakiet wypełnia tę lukę danymi z `@zxcvbn-ts/language-pl`.
71
+
72
+ ### Instalacja i użycie
73
+
74
+ ```bash
75
+ pip install zxcvbn-pl
76
+ ```
77
+
78
+ ```python
79
+ import zxcvbn_pl
80
+ from zxcvbn import zxcvbn
81
+
82
+ zxcvbn_pl.register() # raz, np. przy starcie aplikacji; wielokrotne wywołanie nic nie psuje
83
+
84
+ wynik = zxcvbn("słoneczko123456")
85
+ wynik["score"] # 2
86
+ [m.get("dictionary_name") for m in wynik["sequence"]]
87
+ # ['pl_common_words', 'passwords']
88
+ ```
89
+
90
+ `register()` dokłada trzy słowniki do globalnego rejestru zxcvbn
91
+ (`zxcvbn.matching.RANKED_DICTIONARIES`), więc działa dla każdego kolejnego
92
+ wywołania `zxcvbn()` w procesie:
93
+
94
+ | Nazwa (`dictionary_name`) | Zawartość |
95
+ |---|---|
96
+ | `pl_common_words` | ok. 52 tys. najczęstszych słów (napisy filmowe) |
97
+ | `pl_first_names` | imiona z rejestru PESEL (≥ 200 osób) |
98
+ | `pl_last_names` | nazwiska z rejestru PESEL (≥ 200 osób) |
99
+
100
+ Każde słowo z polskimi znakami jest też w wariancie bez nich (`żółw` →
101
+ `zolw`) z tą samą pozycją. Pomijane są wpisy krótsze niż 3 znaki.
102
+
103
+ Słowniki to indeksy [marisa-trie](https://github.com/pytries/marisa-trie)
104
+ czytane przez mmap (~1 MB na dysku, znikomy narzut RAM).
105
+
106
+ ### Uwaga o wątkach
107
+
108
+ zxcvbn trzyma słowniki — także `user_inputs` przekazane do `zxcvbn()` —
109
+ w globalnym słowniku modułu. `register()` jest bezpieczne, ale jeśli
110
+ wywołujesz `zxcvbn(..., user_inputs=...)` z wielu wątków, owiń wywołanie
111
+ blokadą (to ograniczenie samego zxcvbn, nie tego pakietu).
112
+
113
+ ### Dane i licencje
114
+
115
+ Kod: MIT. Dane pochodzą z `@zxcvbn-ts/language-pl` i mają własne licencje —
116
+ szczegóły w [NOTICE.md](NOTICE.md):
117
+
118
+ - pospolite słowa: OPUS OpenSubtitles 2024 — **ODC-BY** (wymaga podania źródła),
119
+ - imiona i nazwiska: rejestr PESEL, dane.gov.pl — **CC0 1.0**.
120
+
121
+ Odbudowa słowników (deterministyczna, z weryfikacją sha512 paczki npm):
122
+
123
+ ```bash
124
+ uv run python scripts/build_dictionaries.py
125
+ ```
126
+
127
+ ---
128
+
129
+ ## English
130
+
131
+ zxcvbn ships with English dictionaries only, so Polish words look like random
132
+ letters to it and passwords such as `słoneczko123456` or `Katarzyna1985!`
133
+ get the top score. `zxcvbn-pl` adds Polish common words, first names and last
134
+ names (plus diacritics-free variants) to the Python port of zxcvbn:
135
+
136
+ ```python
137
+ import zxcvbn_pl
138
+ from zxcvbn import zxcvbn
139
+
140
+ zxcvbn_pl.register()
141
+ zxcvbn("słoneczko123456")["score"] # 2 instead of 4
142
+ ```
143
+
144
+ `register()` is idempotent and adds three dictionaries (`pl_common_words`,
145
+ `pl_first_names`, `pl_last_names`) to zxcvbn's global
146
+ `zxcvbn.matching.RANKED_DICTIONARIES`. Multi-word passphrases still score
147
+ high, in line with NIST SP 800-63B.
148
+
149
+ The data comes from the `@zxcvbn-ts/language-pl` npm package: common words
150
+ from OPUS OpenSubtitles 2024 (ODC-BY), first and last names from the Polish
151
+ PESEL registry published on dane.gov.pl (CC0 1.0). See [NOTICE.md](NOTICE.md).
152
+ The code is MIT-licensed.
@@ -0,0 +1,123 @@
1
+ # zxcvbn-pl
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/zxcvbn-pl.svg)](https://pypi.org/project/zxcvbn-pl/)
4
+ [![CI](https://github.com/iplweb/zxcvbn-pl/actions/workflows/ci.yml/badge.svg)](https://github.com/iplweb/zxcvbn-pl/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
6
+
7
+ Polskie słowniki dla [zxcvbn](https://github.com/dwolfhub/zxcvbn-python)
8
+ (port w Pythonie) — pospolite słowa, imiona i nazwiska.
9
+
10
+ *Polish dictionaries for the Python port of zxcvbn — common words, first
11
+ names and last names. [English below.](#english)*
12
+
13
+ ---
14
+
15
+ ## 🇵🇱 Po polsku
16
+
17
+ ### Po co
18
+
19
+ zxcvbn szacuje, ile prób potrzeba do odgadnięcia hasła, rozpoznając w nim
20
+ słowa ze słowników, powtórzenia, sekwencje, układy klawiatury i daty. Ma
21
+ jednak **tylko angielskie słowniki** — polskie słowo to dla niego losowe
22
+ litery. Przez to hasła łatwe do zgadnięcia dla każdego, kto ma polską listę
23
+ słów, dostają najwyższą ocenę:
24
+
25
+ | Hasło | zxcvbn | zxcvbn + zxcvbn-pl |
26
+ |---|---|---|
27
+ | `słoneczko123456` | 4 | 2 |
28
+ | `pomidorowa123456` | 3 | 2 |
29
+ | `Katarzyna1985!` | 3 | 2 |
30
+ | `agnieszka.nowak1` | 4 | 3 |
31
+ | `Wojciechowski77` | 2 | 1 |
32
+ | `ptaszek-na-drzewie` | 4 | 4 |
33
+ | `zielonaherbatazcytryna` | 4 | 4 |
34
+
35
+ Frazy z kilku słów nadal są oceniane wysoko — tak jak zaleca NIST
36
+ SP 800-63B. Spada ocena haseł z jednego pospolitego słowa, imienia lub
37
+ nazwiska z dopiskiem. Efekt jest umiarkowany: słowa spoza ok. 50 tys.
38
+ najczęstszych (np. `kaczuszka2000!!`) nadal są dla zxcvbn losowymi literami.
39
+
40
+ Port zxcvbn w JavaScripcie (zxcvbn-ts) ma pakiety językowe; port w Pythonie
41
+ — nie. Ten pakiet wypełnia tę lukę danymi z `@zxcvbn-ts/language-pl`.
42
+
43
+ ### Instalacja i użycie
44
+
45
+ ```bash
46
+ pip install zxcvbn-pl
47
+ ```
48
+
49
+ ```python
50
+ import zxcvbn_pl
51
+ from zxcvbn import zxcvbn
52
+
53
+ zxcvbn_pl.register() # raz, np. przy starcie aplikacji; wielokrotne wywołanie nic nie psuje
54
+
55
+ wynik = zxcvbn("słoneczko123456")
56
+ wynik["score"] # 2
57
+ [m.get("dictionary_name") for m in wynik["sequence"]]
58
+ # ['pl_common_words', 'passwords']
59
+ ```
60
+
61
+ `register()` dokłada trzy słowniki do globalnego rejestru zxcvbn
62
+ (`zxcvbn.matching.RANKED_DICTIONARIES`), więc działa dla każdego kolejnego
63
+ wywołania `zxcvbn()` w procesie:
64
+
65
+ | Nazwa (`dictionary_name`) | Zawartość |
66
+ |---|---|
67
+ | `pl_common_words` | ok. 52 tys. najczęstszych słów (napisy filmowe) |
68
+ | `pl_first_names` | imiona z rejestru PESEL (≥ 200 osób) |
69
+ | `pl_last_names` | nazwiska z rejestru PESEL (≥ 200 osób) |
70
+
71
+ Każde słowo z polskimi znakami jest też w wariancie bez nich (`żółw` →
72
+ `zolw`) z tą samą pozycją. Pomijane są wpisy krótsze niż 3 znaki.
73
+
74
+ Słowniki to indeksy [marisa-trie](https://github.com/pytries/marisa-trie)
75
+ czytane przez mmap (~1 MB na dysku, znikomy narzut RAM).
76
+
77
+ ### Uwaga o wątkach
78
+
79
+ zxcvbn trzyma słowniki — także `user_inputs` przekazane do `zxcvbn()` —
80
+ w globalnym słowniku modułu. `register()` jest bezpieczne, ale jeśli
81
+ wywołujesz `zxcvbn(..., user_inputs=...)` z wielu wątków, owiń wywołanie
82
+ blokadą (to ograniczenie samego zxcvbn, nie tego pakietu).
83
+
84
+ ### Dane i licencje
85
+
86
+ Kod: MIT. Dane pochodzą z `@zxcvbn-ts/language-pl` i mają własne licencje —
87
+ szczegóły w [NOTICE.md](NOTICE.md):
88
+
89
+ - pospolite słowa: OPUS OpenSubtitles 2024 — **ODC-BY** (wymaga podania źródła),
90
+ - imiona i nazwiska: rejestr PESEL, dane.gov.pl — **CC0 1.0**.
91
+
92
+ Odbudowa słowników (deterministyczna, z weryfikacją sha512 paczki npm):
93
+
94
+ ```bash
95
+ uv run python scripts/build_dictionaries.py
96
+ ```
97
+
98
+ ---
99
+
100
+ ## English
101
+
102
+ zxcvbn ships with English dictionaries only, so Polish words look like random
103
+ letters to it and passwords such as `słoneczko123456` or `Katarzyna1985!`
104
+ get the top score. `zxcvbn-pl` adds Polish common words, first names and last
105
+ names (plus diacritics-free variants) to the Python port of zxcvbn:
106
+
107
+ ```python
108
+ import zxcvbn_pl
109
+ from zxcvbn import zxcvbn
110
+
111
+ zxcvbn_pl.register()
112
+ zxcvbn("słoneczko123456")["score"] # 2 instead of 4
113
+ ```
114
+
115
+ `register()` is idempotent and adds three dictionaries (`pl_common_words`,
116
+ `pl_first_names`, `pl_last_names`) to zxcvbn's global
117
+ `zxcvbn.matching.RANKED_DICTIONARIES`. Multi-word passphrases still score
118
+ high, in line with NIST SP 800-63B.
119
+
120
+ The data comes from the `@zxcvbn-ts/language-pl` npm package: common words
121
+ from OPUS OpenSubtitles 2024 (ODC-BY), first and last names from the Polish
122
+ PESEL registry published on dane.gov.pl (CC0 1.0). See [NOTICE.md](NOTICE.md).
123
+ The code is MIT-licensed.
@@ -0,0 +1,59 @@
1
+ [project]
2
+ name = "zxcvbn-pl"
3
+ version = "0.1.0"
4
+ description = "Polish dictionaries (common words, first and last names) for zxcvbn password strength estimation."
5
+ readme = "README.md"
6
+ requires-python = ">=3.9"
7
+ license = { text = "MIT" }
8
+ authors = [{ name = "Michał Pasternak", email = "m@iplweb.pl" }]
9
+ keywords = ["zxcvbn", "password", "password-strength", "polish", "dictionary", "hasło", "security"]
10
+ classifiers = [
11
+ "Development Status :: 4 - Beta",
12
+ "Intended Audience :: Developers",
13
+ "License :: OSI Approved :: MIT License",
14
+ "Natural Language :: Polish",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.9",
17
+ "Programming Language :: Python :: 3.10",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Programming Language :: Python :: 3.13",
21
+ "Programming Language :: Python :: 3.14",
22
+ "Topic :: Security",
23
+ ]
24
+ dependencies = [
25
+ "marisa-trie>=1.0",
26
+ "zxcvbn>=4.4",
27
+ ]
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/iplweb/zxcvbn-pl"
31
+ Repository = "https://github.com/iplweb/zxcvbn-pl"
32
+ Changelog = "https://github.com/iplweb/zxcvbn-pl/blob/main/CHANGELOG.md"
33
+
34
+ [dependency-groups]
35
+ dev = [
36
+ "pytest>=8.0",
37
+ "ruff>=0.6",
38
+ "pre-commit>=3.5",
39
+ ]
40
+
41
+ [build-system]
42
+ requires = ["hatchling"]
43
+ build-backend = "hatchling.build"
44
+
45
+ [tool.hatch.build.targets.wheel]
46
+ packages = ["src/zxcvbn_pl"]
47
+
48
+ [tool.hatch.build.targets.sdist]
49
+ only-include = ["src", "tests", "scripts", "README.md", "LICENSE", "NOTICE.md", "CHANGELOG.md"]
50
+
51
+ [tool.ruff]
52
+ line-length = 88
53
+ target-version = "py39"
54
+
55
+ [tool.ruff.lint]
56
+ select = ["E", "F", "W", "I", "B", "UP"]
57
+
58
+ [tool.pytest.ini_options]
59
+ testpaths = ["tests"]
@@ -0,0 +1,104 @@
1
+ """Build the Polish zxcvbn dictionaries shipped in ``src/zxcvbn_pl/data``.
2
+
3
+ Source data: the ``@zxcvbn-ts/language-pl`` npm package (pinned version,
4
+ verified against the registry's sha512 integrity). Each upstream list is
5
+ ordered by frequency; the position becomes the zxcvbn rank. Male and female
6
+ lists are merged (lower rank wins). For every word with Polish diacritics a
7
+ variant without them is added with the same rank (``żółw`` -> ``zolw``) —
8
+ that is how such words are usually typed in passwords.
9
+
10
+ Run from the repository root (needs only the runtime dependency)::
11
+
12
+ uv run python scripts/build_dictionaries.py
13
+
14
+ The output is deterministic: the same upstream version gives byte-identical
15
+ ``.marisa`` files.
16
+ """
17
+
18
+ import base64
19
+ import hashlib
20
+ import io
21
+ import json
22
+ import tarfile
23
+ import unicodedata
24
+ import urllib.request
25
+ from pathlib import Path
26
+
27
+ import marisa_trie
28
+
29
+ UPSTREAM_VERSION = "4.1.1"
30
+ UPSTREAM_URL = (
31
+ "https://registry.npmjs.org/@zxcvbn-ts/language-pl/-/"
32
+ f"language-pl-{UPSTREAM_VERSION}.tgz"
33
+ )
34
+ UPSTREAM_SHA512 = (
35
+ "hF6Qu9cyHx7sSEzNOQrJQntq8geoincsGvlOC0wkD43LnUVSTR49MKxdsifsemlQgtxR7aUK"
36
+ "DgK/e/RFHOljoQ=="
37
+ )
38
+
39
+ # Output dictionary name -> upstream lists merged into it.
40
+ DICTIONARIES = {
41
+ "pl_common_words": ["commonWords"],
42
+ "pl_first_names": ["femaleFirstnames", "maleFirstnames"],
43
+ "pl_last_names": ["femaleLastnames", "maleLastnames"],
44
+ }
45
+
46
+ # Single letters and two-letter tokens match everywhere and only add noise.
47
+ MIN_LENGTH = 3
48
+
49
+ OUTPUT_DIR = Path(__file__).resolve().parent.parent / "src" / "zxcvbn_pl" / "data"
50
+
51
+
52
+ def strip_diacritics(word):
53
+ word = word.replace("ł", "l")
54
+ word = unicodedata.normalize("NFKD", word)
55
+ return "".join(c for c in word if not unicodedata.combining(c))
56
+
57
+
58
+ def download():
59
+ with urllib.request.urlopen(UPSTREAM_URL, timeout=60) as response:
60
+ payload = response.read()
61
+ digest = base64.b64encode(hashlib.sha512(payload).digest()).decode()
62
+ if digest != UPSTREAM_SHA512:
63
+ raise RuntimeError(
64
+ f"sha512 mismatch for {UPSTREAM_URL}: got {digest}, "
65
+ f"expected {UPSTREAM_SHA512}"
66
+ )
67
+ return payload
68
+
69
+
70
+ def read_lists(payload):
71
+ lists = {}
72
+ with tarfile.open(fileobj=io.BytesIO(payload), mode="r:gz") as tar:
73
+ for name in {n for names in DICTIONARIES.values() for n in names}:
74
+ member = tar.extractfile(f"package/src/{name}.json")
75
+ lists[name] = json.load(member)
76
+ return lists
77
+
78
+
79
+ def build_ranks(upstream_lists):
80
+ ranks = {}
81
+ for words in upstream_lists:
82
+ for position, word in enumerate(words, start=1):
83
+ word = word.strip().lower()
84
+ if len(word) < MIN_LENGTH or not word.isalpha():
85
+ continue
86
+ for variant in (word, strip_diacritics(word)):
87
+ if position < ranks.get(variant, position + 1):
88
+ ranks[variant] = position
89
+ return ranks
90
+
91
+
92
+ def main():
93
+ lists = read_lists(download())
94
+ OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
95
+ for dictionary, sources in DICTIONARIES.items():
96
+ ranks = build_ranks(lists[source] for source in sources)
97
+ trie = marisa_trie.RecordTrie("<I", ((w, (r,)) for w, r in ranks.items()))
98
+ path = OUTPUT_DIR / f"{dictionary}.marisa"
99
+ trie.save(str(path))
100
+ print(f"{path.name}: {len(trie)} entries, {path.stat().st_size // 1024} KB")
101
+
102
+
103
+ if __name__ == "__main__":
104
+ main()
@@ -0,0 +1,78 @@
1
+ """Polish dictionaries for zxcvbn (the Python port).
2
+
3
+ zxcvbn ships English dictionaries only, so to zxcvbn a Polish word looks like
4
+ random letters and passwords such as ``słoneczko123456`` or
5
+ ``Katarzyna1985!`` get the top score. This package adds Polish common words,
6
+ first names and last names (with diacritics-free variants) to zxcvbn::
7
+
8
+ import zxcvbn_pl
9
+ from zxcvbn import zxcvbn
10
+
11
+ zxcvbn_pl.register()
12
+ zxcvbn("słoneczko123456")["score"] # 2 instead of 4
13
+
14
+ The dictionaries are marisa-trie indices read via mmap (no big RAM cost).
15
+ """
16
+
17
+ import threading
18
+ from pathlib import Path
19
+
20
+ import marisa_trie
21
+
22
+ __all__ = ["DICTIONARY_NAMES", "MarisaRankedDictionary", "dictionaries", "register"]
23
+ __version__ = "0.1.0"
24
+
25
+ DATA_DIR = Path(__file__).parent / "data"
26
+
27
+ #: Names under which the dictionaries are registered in zxcvbn — they show up
28
+ #: as ``dictionary_name`` in zxcvbn's match sequence.
29
+ DICTIONARY_NAMES = ("pl_common_words", "pl_first_names", "pl_last_names")
30
+
31
+ _lock = threading.Lock()
32
+ _dictionaries = None
33
+
34
+
35
+ class MarisaRankedDictionary:
36
+ """Read-only ``word -> rank`` mapping over a marisa-trie index.
37
+
38
+ zxcvbn only needs ``word in d`` and ``d[word]`` from a ranked dictionary,
39
+ so this is a drop-in replacement for the plain dicts it builds itself.
40
+ """
41
+
42
+ def __init__(self, path):
43
+ self._trie = marisa_trie.RecordTrie("<I").mmap(str(path))
44
+
45
+ def __contains__(self, word):
46
+ return word in self._trie
47
+
48
+ def __getitem__(self, word):
49
+ return self._trie[word][0][0]
50
+
51
+ def __len__(self):
52
+ return len(self._trie)
53
+
54
+ def get(self, word, default=None):
55
+ return self[word] if word in self else default
56
+
57
+
58
+ def dictionaries():
59
+ """Return ``{name: MarisaRankedDictionary}`` for all Polish dictionaries."""
60
+ global _dictionaries
61
+ with _lock:
62
+ if _dictionaries is None:
63
+ _dictionaries = {
64
+ name: MarisaRankedDictionary(DATA_DIR / f"{name}.marisa")
65
+ for name in DICTIONARY_NAMES
66
+ }
67
+ return _dictionaries
68
+
69
+
70
+ def register():
71
+ """Add the Polish dictionaries to zxcvbn. Safe to call many times.
72
+
73
+ zxcvbn keeps its ranked dictionaries in a module-level dict, so this
74
+ affects every later ``zxcvbn()`` call in the process.
75
+ """
76
+ from zxcvbn import matching
77
+
78
+ matching.RANKED_DICTIONARIES.update(dictionaries())
File without changes
@@ -0,0 +1,78 @@
1
+ import pytest
2
+ from zxcvbn import matching, zxcvbn
3
+
4
+ import zxcvbn_pl
5
+
6
+
7
+ @pytest.fixture
8
+ def zarejestrowane():
9
+ przed = dict(matching.RANKED_DICTIONARIES)
10
+ zxcvbn_pl.register()
11
+ yield
12
+ matching.RANKED_DICTIONARIES.clear()
13
+ matching.RANKED_DICTIONARIES.update(przed)
14
+
15
+
16
+ def test_slowniki_maja_dane():
17
+ slowniki = zxcvbn_pl.dictionaries()
18
+ assert set(slowniki) == set(zxcvbn_pl.DICTIONARY_NAMES)
19
+ assert all(len(slownik) > 1000 for slownik in slowniki.values())
20
+
21
+
22
+ @pytest.mark.parametrize(
23
+ "nazwa,slowo",
24
+ [
25
+ ("pl_common_words", "kot"),
26
+ ("pl_first_names", "katarzyna"),
27
+ ("pl_last_names", "kowalski"),
28
+ ("pl_last_names", "kowalska"),
29
+ ],
30
+ )
31
+ def test_slowo_w_slowniku(nazwa, slowo):
32
+ assert slowo in zxcvbn_pl.dictionaries()[nazwa]
33
+
34
+
35
+ def test_wariant_bez_polskich_znakow_ma_te_sama_pozycje():
36
+ slownik = zxcvbn_pl.dictionaries()["pl_common_words"]
37
+ assert slownik["zolw"] == slownik["żółw"]
38
+ assert slownik["slonce"] == slownik["słońce"]
39
+
40
+
41
+ def test_czestsze_slowo_ma_nizsza_pozycje():
42
+ slownik = zxcvbn_pl.dictionaries()["pl_common_words"]
43
+ assert slownik["nie"] < slownik["kot"] < slownik["żółw"]
44
+
45
+
46
+ def test_brak_krotkich_i_nieliterowych_wpisow():
47
+ slownik = zxcvbn_pl.dictionaries()["pl_common_words"]
48
+ assert "na" not in slownik
49
+ assert "xqzvw" not in slownik
50
+ assert slownik.get("xqzvw") is None
51
+
52
+
53
+ def test_register_jest_idempotentne(zarejestrowane):
54
+ zxcvbn_pl.register()
55
+ for nazwa in zxcvbn_pl.DICTIONARY_NAMES:
56
+ assert matching.RANKED_DICTIONARIES[nazwa] is zxcvbn_pl.dictionaries()[nazwa]
57
+
58
+
59
+ @pytest.mark.parametrize(
60
+ "haslo,slownik",
61
+ [
62
+ ("słoneczko123456", "pl_common_words"),
63
+ ("Katarzyna1985!", "pl_first_names"),
64
+ ("Wojciechowski77", "pl_last_names"),
65
+ ],
66
+ )
67
+ def test_polskie_slowa_obnizaja_ocene(zarejestrowane, haslo, slownik):
68
+ wynik = zxcvbn(haslo)
69
+ assert wynik["score"] <= 2
70
+ assert slownik in [m.get("dictionary_name") for m in wynik["sequence"]]
71
+
72
+
73
+ def test_bez_rejestracji_zxcvbn_nie_zna_polskich_slow():
74
+ assert zxcvbn("słoneczko123456")["score"] == 4
75
+
76
+
77
+ def test_fraza_z_kilku_slow_nadal_mocna(zarejestrowane):
78
+ assert zxcvbn("zielonaherbatazcytryna")["score"] == 4