shevchenko-py 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,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Misha (Python port)
4
+ Copyright (c) 2017 Oleksandr Tolochko <shevchenko-js@tooleks.com>, Tetyana Tolochko, Anna Tolochko (shevchenko-js, from which the declension rules, gender rules, classifier model, and test data are derived)
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
@@ -0,0 +1,4 @@
1
+ prune tests
2
+ prune tools
3
+ prune reference
4
+ exclude benchmark.py
@@ -0,0 +1,266 @@
1
+ Metadata-Version: 2.4
2
+ Name: shevchenko-py
3
+ Version: 0.1.0
4
+ Summary: Zero-dependency Ukrainian declension of personal names, military ranks, and appointments (port of shevchenko-js). Import name: shevchenko.
5
+ Author: Misha
6
+ License: MIT
7
+ Project-URL: Upstream, https://github.com/tooleks/shevchenko-js
8
+ Keywords: ukrainian,declension,grammatical-cases,names,military
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Natural Language :: Ukrainian
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Topic :: Text Processing :: Linguistic
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Dynamic: license-file
21
+
22
+ # shevchenko-py
23
+
24
+ Ukrainian grammatical-case declension of **personal names**, **military ranks**, and
25
+ **military appointments**. A minimal, dependency-free Python port of
26
+ [shevchenko-js](https://github.com/tooleks/shevchenko-js) and
27
+ [shevchenko-ext-military](https://github.com/tooleks/shevchenko-ext-military).
28
+
29
+ - Zero runtime dependencies (pure Python ≥ 3.9, stdlib only)
30
+ - Tiny: ~30 KB wheel, ~116 KB installed (18 KB code + 98 KB data), 8 ms import
31
+ - All 7 Ukrainian grammatical cases
32
+ - Gender auto-detection from the patronymic or given name
33
+ - The upstream ML surname classifier reimplemented as ~30 lines of pure Python
34
+
35
+ ## Install
36
+
37
+ ```sh
38
+ pip install shevchenko-py
39
+ ```
40
+
41
+ The distribution is named `shevchenko-py`; the import name is `shevchenko`.
42
+ Don't install this alongside the unrelated [`shevchenko`](https://pypi.org/project/shevchenko/)
43
+ PyPI package (a different port, anthroponyms only) — both provide the
44
+ `shevchenko` module.
45
+
46
+ ## Quick start
47
+
48
+ ```python
49
+ import shevchenko
50
+
51
+ shevchenko.in_genitive(
52
+ given_name="Тарас",
53
+ patronymic_name="Григорович",
54
+ family_name="Шевченко",
55
+ military_rank="старший солдат",
56
+ military_appointment="заступник командира взводу",
57
+ )
58
+ # {'given_name': 'Тараса', 'patronymic_name': 'Григоровича',
59
+ # 'family_name': 'Шевченка', 'military_rank': 'старшого солдата',
60
+ # 'military_appointment': 'заступника командира взводу'}
61
+ ```
62
+
63
+ ## API reference
64
+
65
+ Everything is importable from the top-level `shevchenko` package. Anything
66
+ prefixed with `_` (modules `_inflector`, `_names`, …) is internal and not a
67
+ stable interface.
68
+
69
+ ### Declension functions
70
+
71
+ ```python
72
+ shevchenko.in_nominative(input=None, **kwargs) -> dict # називний (хто? що?)
73
+ shevchenko.in_genitive(input=None, **kwargs) -> dict # родовий (кого? чого?)
74
+ shevchenko.in_dative(input=None, **kwargs) -> dict # давальний (кому? чому?)
75
+ shevchenko.in_accusative(input=None, **kwargs) -> dict # знахідний (кого? що?)
76
+ shevchenko.in_ablative(input=None, **kwargs) -> dict # орудний (ким? чим?)
77
+ shevchenko.in_locative(input=None, **kwargs) -> dict # місцевий (на кому? на чому?)
78
+ shevchenko.in_vocative(input=None, **kwargs) -> dict # кличний (звертання)
79
+
80
+ shevchenko.in_case(case, input=None, **kwargs) -> dict
81
+ ```
82
+
83
+ All seven are thin wrappers around `in_case` with the case fixed. Upstream calls
84
+ the instrumental case (орудний) "ablative"; this port keeps that naming.
85
+
86
+ `in_case(case, ...)` takes the case as a string — one of `shevchenko.CASES`
87
+ (`"nominative"`, `"genitive"`, `"dative"`, `"accusative"`, `"ablative"`,
88
+ `"locative"`, `"vocative"`) — useful when the case is chosen at runtime:
89
+
90
+ ```python
91
+ for case in shevchenko.CASES:
92
+ print(shevchenko.in_case(case, gender="masculine", given_name="Тарас"))
93
+ ```
94
+
95
+ #### Input
96
+
97
+ Fields may be passed as a dict, as keyword arguments, or both (keyword
98
+ arguments override the dict):
99
+
100
+ ```python
101
+ shevchenko.in_vocative({"given_name": "Тарас"}, family_name="Шевченко")
102
+ # {'given_name': 'Тарасе', 'family_name': 'Шевченку'}
103
+ ```
104
+
105
+ | Field | Meaning | Example |
106
+ |---|---|---|
107
+ | `given_name` | First name (ім'я) | `"Тарас"` |
108
+ | `patronymic_name` | Patronymic (по батькові) | `"Григорович"` |
109
+ | `family_name` | Surname (прізвище) | `"Шевченко"` |
110
+ | `military_rank` | Rank phrase (військове звання) | `"старший солдат"` |
111
+ | `military_appointment` | Appointment/position phrase (посада) | `"заступник командира взводу"` |
112
+ | `gender` | `"masculine"` / `"feminine"`; optional, see below | `"masculine"` |
113
+
114
+ All fields are optional strings, but at least one non-`gender` field is
115
+ required. Unknown keys raise `InputValidationError` (so `givenName=` typos are
116
+ caught, not silently ignored). Values are NFC-normalized before matching.
117
+
118
+ #### The `gender` parameter
119
+
120
+ `gender` applies to the three name fields. If omitted, it is auto-detected from
121
+ the patronymic (preferred) or the given name — same logic as
122
+ [`detect_gender`](#detect_gender). Two consequences:
123
+
124
+ - Input with a patronymic or a recognizable given name never needs `gender`.
125
+ - A lone `family_name` usually can't be sexed — pass `gender` explicitly or
126
+ `InputValidationError` is raised.
127
+
128
+ Military fields don't need `gender` at all: each word inside a rank or
129
+ appointment phrase carries its own grammatical gender (`медична сестра`
130
+ declines as feminine regardless of the person).
131
+
132
+ #### Return value
133
+
134
+ A dict containing **only the fields you passed in** (never `gender`), each
135
+ value inflected in the requested case. Field order is fixed: `given_name`,
136
+ `patronymic_name`, `family_name`, `military_rank`, `military_appointment`.
137
+
138
+ #### Behavior notes
139
+
140
+ - **Letter case is preserved**: `ШЕВЧЕНКО` → `ШЕВЧЕНКА`, `Шевченко` → `Шевченка`.
141
+ - **Hyphenated names** decline part by part (`Нечуй-Левицький` →
142
+ `Нечуя-Левицького`); monosyllabic non-final surname parts stay frozen
143
+ (`Драй-Хмара` → `Драй-Хмари`).
144
+ - **Unknown words in military phrases** — proper names, abbreviations,
145
+ qualifiers already in genitive — pass through unchanged:
146
+
147
+ ```python
148
+ shevchenko.in_ablative(military_appointment='оператор БПЛА "Фурія"')
149
+ # {'military_appointment': 'оператором БПЛА "Фурія"'}
150
+ ```
151
+ - **Unknown names** that no declension rule matches are returned unchanged
152
+ rather than guessed at.
153
+
154
+ #### Errors
155
+
156
+ `InputValidationError` (subclass of `ValueError`) is raised when:
157
+
158
+ - `case` is not one of `CASES` (for `in_case`),
159
+ - no field besides `gender` is provided,
160
+ - a field value is not a string,
161
+ - an unknown parameter is passed,
162
+ - `gender` is neither `"masculine"`, `"feminine"`, nor omitted,
163
+ - `gender` is omitted for name fields and cannot be auto-detected.
164
+
165
+ ### `detect_gender`
166
+
167
+ ```python
168
+ shevchenko.detect_gender(input=None, **kwargs) -> str | None
169
+ ```
170
+
171
+ Detects the grammatical gender from `patronymic_name` (checked first) or
172
+ `given_name` endings. Accepts `family_name` too, but a surname alone is never
173
+ used for detection. Returns `"masculine"`, `"feminine"`, or `None` when
174
+ undetectable — unlike the declension functions it does not raise on ambiguity.
175
+
176
+ ```python
177
+ shevchenko.detect_gender(patronymic_name="Григорович") # 'masculine'
178
+ shevchenko.detect_gender(given_name="Оксана") # 'feminine'
179
+ shevchenko.detect_gender(family_name="Шевченко") # None
180
+ ```
181
+
182
+ Input rules are the same as for declension: dict and/or kwargs, at least one
183
+ field, strings only, unknown keys raise `InputValidationError`.
184
+
185
+ ### Constants
186
+
187
+ | Constant | Value |
188
+ |---|---|
189
+ | `shevchenko.CASES` | `("nominative", "genitive", "dative", "accusative", "ablative", "locative", "vocative")` |
190
+ | `shevchenko.NOMINATIVE` … `shevchenko.VOCATIVE` | The individual case strings |
191
+ | `shevchenko.GENDERS` | `("masculine", "feminine")` |
192
+ | `shevchenko.MASCULINE`, `shevchenko.FEMININE` | The individual gender strings |
193
+ | `shevchenko.__version__` | Package version string |
194
+
195
+ The constants are plain strings/tuples, so `gender="feminine"` and
196
+ `gender=shevchenko.FEMININE` are interchangeable.
197
+
198
+ ### `InputValidationError`
199
+
200
+ ```python
201
+ class InputValidationError(ValueError)
202
+ ```
203
+
204
+ Raised by all public functions on invalid input; see [Errors](#errors).
205
+
206
+ ## Performance
207
+
208
+ Measured on Python 3.14 with `python benchmark.py` (CPython, Windows, single
209
+ thread):
210
+
211
+ | Operation | Speed |
212
+ |---|---|
213
+ | Cold import (data load; regexes compile lazily) | ~8 ms, once |
214
+ | Full person (3 names + rank + appointment), 1 case | ~140 µs (≈7,000/s) |
215
+ | Single name field | ~20–24 µs (≈42–50,000/s) |
216
+ | Military rank / appointment phrase | ~40–50 µs (≈21–25,000/s) |
217
+ | `detect_gender` | ~1 µs (≈700,000/s) |
218
+ | Ambiguous surname, first time (pure-Python RNN) | ~0.9 ms |
219
+ | Ambiguous surname, repeated (memoized) | ~10 µs |
220
+ | Real-corpus throughput (name triple, one case) | ≈13,000/s |
221
+
222
+ The only slow path is the first classification of an ambiguous surname (the
223
+ neural network runs in pure Python); results are memoized, so each unique
224
+ surname pays it once per process.
225
+
226
+ ## Fidelity
227
+
228
+ Tested against the complete upstream corpora (2334 fixtures: every anthroponym
229
+ test case, every rank and appointment in all 7 cases, and the 1144-name gender
230
+ detection dataset) and differentially verified against the live JS library on
231
+ 3990 additional word/case combinations — zero mismatches. All upstream
232
+ functional unit tests (letter case, syllables, alphabet encoding, classifier
233
+ input encoding and decode threshold) are ported; upstream's input-validation
234
+ suite is not, since this port's API surface deliberately differs.
235
+
236
+ ## How it works
237
+
238
+ Declension is rule-based: 99 regex rules from upstream (`shevchenko/data/`)
239
+ keyed by gender, word class, and usage, applied by priority. Surnames whose word
240
+ class is ambiguous (e.g. feminine `-а/-я`, masculine `-ий/-ой/-их`) are
241
+ classified noun-vs-adjective by the upstream neural network — an
242
+ Embedding→SimpleRNN(16)→Dense model small enough (4 KB) to run in pure Python
243
+ (`shevchenko/_classifier.py`). Two upstream regexes use variable-width
244
+ lookbehinds unsupported by Python's `re` and are rewritten to equivalent forms at
245
+ load time (`shevchenko/_inflector.py`).
246
+
247
+ The runtime data is regenerated from the upstream sources by
248
+ `tools/build_data.py` (dev-only): it strips documentation-only fields, drops
249
+ empty case entries, minifies, and verifies every pattern compiles under
250
+ Python's `re`. Data loading uses plain file reads instead of
251
+ `importlib.resources`, whose import chain alone costs ~25 ms.
252
+
253
+ ## Differences from shevchenko-js
254
+
255
+ | | shevchenko-js | shevchenko-py |
256
+ |---|---|---|
257
+ | Field names | `givenName`, `familyName`, … | `given_name`, `family_name`, … |
258
+ | Military fields | separate extension, `registerExtension(...)` | built in |
259
+ | `gender` | always required | auto-detected when omitted |
260
+ | API style | `async`/`await` | plain synchronous calls |
261
+ | Validation errors | `InputValidationError extends TypeError` | `InputValidationError(ValueError)` |
262
+
263
+ ## License
264
+
265
+ MIT. Declension rules, gender rules, model weights, and test corpora are from
266
+ shevchenko-js © Oleksandr Tolochko et al., MIT-licensed.
@@ -0,0 +1,245 @@
1
+ # shevchenko-py
2
+
3
+ Ukrainian grammatical-case declension of **personal names**, **military ranks**, and
4
+ **military appointments**. A minimal, dependency-free Python port of
5
+ [shevchenko-js](https://github.com/tooleks/shevchenko-js) and
6
+ [shevchenko-ext-military](https://github.com/tooleks/shevchenko-ext-military).
7
+
8
+ - Zero runtime dependencies (pure Python ≥ 3.9, stdlib only)
9
+ - Tiny: ~30 KB wheel, ~116 KB installed (18 KB code + 98 KB data), 8 ms import
10
+ - All 7 Ukrainian grammatical cases
11
+ - Gender auto-detection from the patronymic or given name
12
+ - The upstream ML surname classifier reimplemented as ~30 lines of pure Python
13
+
14
+ ## Install
15
+
16
+ ```sh
17
+ pip install shevchenko-py
18
+ ```
19
+
20
+ The distribution is named `shevchenko-py`; the import name is `shevchenko`.
21
+ Don't install this alongside the unrelated [`shevchenko`](https://pypi.org/project/shevchenko/)
22
+ PyPI package (a different port, anthroponyms only) — both provide the
23
+ `shevchenko` module.
24
+
25
+ ## Quick start
26
+
27
+ ```python
28
+ import shevchenko
29
+
30
+ shevchenko.in_genitive(
31
+ given_name="Тарас",
32
+ patronymic_name="Григорович",
33
+ family_name="Шевченко",
34
+ military_rank="старший солдат",
35
+ military_appointment="заступник командира взводу",
36
+ )
37
+ # {'given_name': 'Тараса', 'patronymic_name': 'Григоровича',
38
+ # 'family_name': 'Шевченка', 'military_rank': 'старшого солдата',
39
+ # 'military_appointment': 'заступника командира взводу'}
40
+ ```
41
+
42
+ ## API reference
43
+
44
+ Everything is importable from the top-level `shevchenko` package. Anything
45
+ prefixed with `_` (modules `_inflector`, `_names`, …) is internal and not a
46
+ stable interface.
47
+
48
+ ### Declension functions
49
+
50
+ ```python
51
+ shevchenko.in_nominative(input=None, **kwargs) -> dict # називний (хто? що?)
52
+ shevchenko.in_genitive(input=None, **kwargs) -> dict # родовий (кого? чого?)
53
+ shevchenko.in_dative(input=None, **kwargs) -> dict # давальний (кому? чому?)
54
+ shevchenko.in_accusative(input=None, **kwargs) -> dict # знахідний (кого? що?)
55
+ shevchenko.in_ablative(input=None, **kwargs) -> dict # орудний (ким? чим?)
56
+ shevchenko.in_locative(input=None, **kwargs) -> dict # місцевий (на кому? на чому?)
57
+ shevchenko.in_vocative(input=None, **kwargs) -> dict # кличний (звертання)
58
+
59
+ shevchenko.in_case(case, input=None, **kwargs) -> dict
60
+ ```
61
+
62
+ All seven are thin wrappers around `in_case` with the case fixed. Upstream calls
63
+ the instrumental case (орудний) "ablative"; this port keeps that naming.
64
+
65
+ `in_case(case, ...)` takes the case as a string — one of `shevchenko.CASES`
66
+ (`"nominative"`, `"genitive"`, `"dative"`, `"accusative"`, `"ablative"`,
67
+ `"locative"`, `"vocative"`) — useful when the case is chosen at runtime:
68
+
69
+ ```python
70
+ for case in shevchenko.CASES:
71
+ print(shevchenko.in_case(case, gender="masculine", given_name="Тарас"))
72
+ ```
73
+
74
+ #### Input
75
+
76
+ Fields may be passed as a dict, as keyword arguments, or both (keyword
77
+ arguments override the dict):
78
+
79
+ ```python
80
+ shevchenko.in_vocative({"given_name": "Тарас"}, family_name="Шевченко")
81
+ # {'given_name': 'Тарасе', 'family_name': 'Шевченку'}
82
+ ```
83
+
84
+ | Field | Meaning | Example |
85
+ |---|---|---|
86
+ | `given_name` | First name (ім'я) | `"Тарас"` |
87
+ | `patronymic_name` | Patronymic (по батькові) | `"Григорович"` |
88
+ | `family_name` | Surname (прізвище) | `"Шевченко"` |
89
+ | `military_rank` | Rank phrase (військове звання) | `"старший солдат"` |
90
+ | `military_appointment` | Appointment/position phrase (посада) | `"заступник командира взводу"` |
91
+ | `gender` | `"masculine"` / `"feminine"`; optional, see below | `"masculine"` |
92
+
93
+ All fields are optional strings, but at least one non-`gender` field is
94
+ required. Unknown keys raise `InputValidationError` (so `givenName=` typos are
95
+ caught, not silently ignored). Values are NFC-normalized before matching.
96
+
97
+ #### The `gender` parameter
98
+
99
+ `gender` applies to the three name fields. If omitted, it is auto-detected from
100
+ the patronymic (preferred) or the given name — same logic as
101
+ [`detect_gender`](#detect_gender). Two consequences:
102
+
103
+ - Input with a patronymic or a recognizable given name never needs `gender`.
104
+ - A lone `family_name` usually can't be sexed — pass `gender` explicitly or
105
+ `InputValidationError` is raised.
106
+
107
+ Military fields don't need `gender` at all: each word inside a rank or
108
+ appointment phrase carries its own grammatical gender (`медична сестра`
109
+ declines as feminine regardless of the person).
110
+
111
+ #### Return value
112
+
113
+ A dict containing **only the fields you passed in** (never `gender`), each
114
+ value inflected in the requested case. Field order is fixed: `given_name`,
115
+ `patronymic_name`, `family_name`, `military_rank`, `military_appointment`.
116
+
117
+ #### Behavior notes
118
+
119
+ - **Letter case is preserved**: `ШЕВЧЕНКО` → `ШЕВЧЕНКА`, `Шевченко` → `Шевченка`.
120
+ - **Hyphenated names** decline part by part (`Нечуй-Левицький` →
121
+ `Нечуя-Левицького`); monosyllabic non-final surname parts stay frozen
122
+ (`Драй-Хмара` → `Драй-Хмари`).
123
+ - **Unknown words in military phrases** — proper names, abbreviations,
124
+ qualifiers already in genitive — pass through unchanged:
125
+
126
+ ```python
127
+ shevchenko.in_ablative(military_appointment='оператор БПЛА "Фурія"')
128
+ # {'military_appointment': 'оператором БПЛА "Фурія"'}
129
+ ```
130
+ - **Unknown names** that no declension rule matches are returned unchanged
131
+ rather than guessed at.
132
+
133
+ #### Errors
134
+
135
+ `InputValidationError` (subclass of `ValueError`) is raised when:
136
+
137
+ - `case` is not one of `CASES` (for `in_case`),
138
+ - no field besides `gender` is provided,
139
+ - a field value is not a string,
140
+ - an unknown parameter is passed,
141
+ - `gender` is neither `"masculine"`, `"feminine"`, nor omitted,
142
+ - `gender` is omitted for name fields and cannot be auto-detected.
143
+
144
+ ### `detect_gender`
145
+
146
+ ```python
147
+ shevchenko.detect_gender(input=None, **kwargs) -> str | None
148
+ ```
149
+
150
+ Detects the grammatical gender from `patronymic_name` (checked first) or
151
+ `given_name` endings. Accepts `family_name` too, but a surname alone is never
152
+ used for detection. Returns `"masculine"`, `"feminine"`, or `None` when
153
+ undetectable — unlike the declension functions it does not raise on ambiguity.
154
+
155
+ ```python
156
+ shevchenko.detect_gender(patronymic_name="Григорович") # 'masculine'
157
+ shevchenko.detect_gender(given_name="Оксана") # 'feminine'
158
+ shevchenko.detect_gender(family_name="Шевченко") # None
159
+ ```
160
+
161
+ Input rules are the same as for declension: dict and/or kwargs, at least one
162
+ field, strings only, unknown keys raise `InputValidationError`.
163
+
164
+ ### Constants
165
+
166
+ | Constant | Value |
167
+ |---|---|
168
+ | `shevchenko.CASES` | `("nominative", "genitive", "dative", "accusative", "ablative", "locative", "vocative")` |
169
+ | `shevchenko.NOMINATIVE` … `shevchenko.VOCATIVE` | The individual case strings |
170
+ | `shevchenko.GENDERS` | `("masculine", "feminine")` |
171
+ | `shevchenko.MASCULINE`, `shevchenko.FEMININE` | The individual gender strings |
172
+ | `shevchenko.__version__` | Package version string |
173
+
174
+ The constants are plain strings/tuples, so `gender="feminine"` and
175
+ `gender=shevchenko.FEMININE` are interchangeable.
176
+
177
+ ### `InputValidationError`
178
+
179
+ ```python
180
+ class InputValidationError(ValueError)
181
+ ```
182
+
183
+ Raised by all public functions on invalid input; see [Errors](#errors).
184
+
185
+ ## Performance
186
+
187
+ Measured on Python 3.14 with `python benchmark.py` (CPython, Windows, single
188
+ thread):
189
+
190
+ | Operation | Speed |
191
+ |---|---|
192
+ | Cold import (data load; regexes compile lazily) | ~8 ms, once |
193
+ | Full person (3 names + rank + appointment), 1 case | ~140 µs (≈7,000/s) |
194
+ | Single name field | ~20–24 µs (≈42–50,000/s) |
195
+ | Military rank / appointment phrase | ~40–50 µs (≈21–25,000/s) |
196
+ | `detect_gender` | ~1 µs (≈700,000/s) |
197
+ | Ambiguous surname, first time (pure-Python RNN) | ~0.9 ms |
198
+ | Ambiguous surname, repeated (memoized) | ~10 µs |
199
+ | Real-corpus throughput (name triple, one case) | ≈13,000/s |
200
+
201
+ The only slow path is the first classification of an ambiguous surname (the
202
+ neural network runs in pure Python); results are memoized, so each unique
203
+ surname pays it once per process.
204
+
205
+ ## Fidelity
206
+
207
+ Tested against the complete upstream corpora (2334 fixtures: every anthroponym
208
+ test case, every rank and appointment in all 7 cases, and the 1144-name gender
209
+ detection dataset) and differentially verified against the live JS library on
210
+ 3990 additional word/case combinations — zero mismatches. All upstream
211
+ functional unit tests (letter case, syllables, alphabet encoding, classifier
212
+ input encoding and decode threshold) are ported; upstream's input-validation
213
+ suite is not, since this port's API surface deliberately differs.
214
+
215
+ ## How it works
216
+
217
+ Declension is rule-based: 99 regex rules from upstream (`shevchenko/data/`)
218
+ keyed by gender, word class, and usage, applied by priority. Surnames whose word
219
+ class is ambiguous (e.g. feminine `-а/-я`, masculine `-ий/-ой/-их`) are
220
+ classified noun-vs-adjective by the upstream neural network — an
221
+ Embedding→SimpleRNN(16)→Dense model small enough (4 KB) to run in pure Python
222
+ (`shevchenko/_classifier.py`). Two upstream regexes use variable-width
223
+ lookbehinds unsupported by Python's `re` and are rewritten to equivalent forms at
224
+ load time (`shevchenko/_inflector.py`).
225
+
226
+ The runtime data is regenerated from the upstream sources by
227
+ `tools/build_data.py` (dev-only): it strips documentation-only fields, drops
228
+ empty case entries, minifies, and verifies every pattern compiles under
229
+ Python's `re`. Data loading uses plain file reads instead of
230
+ `importlib.resources`, whose import chain alone costs ~25 ms.
231
+
232
+ ## Differences from shevchenko-js
233
+
234
+ | | shevchenko-js | shevchenko-py |
235
+ |---|---|---|
236
+ | Field names | `givenName`, `familyName`, … | `given_name`, `family_name`, … |
237
+ | Military fields | separate extension, `registerExtension(...)` | built in |
238
+ | `gender` | always required | auto-detected when omitted |
239
+ | API style | `async`/`await` | plain synchronous calls |
240
+ | Validation errors | `InputValidationError extends TypeError` | `InputValidationError(ValueError)` |
241
+
242
+ ## License
243
+
244
+ MIT. Declension rules, gender rules, model weights, and test corpora are from
245
+ shevchenko-js © Oleksandr Tolochko et al., MIT-licensed.
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "shevchenko-py"
7
+ version = "0.1.0"
8
+ description = "Zero-dependency Ukrainian declension of personal names, military ranks, and appointments (port of shevchenko-js). Import name: shevchenko."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ authors = [{ name = "Misha" }]
12
+ requires-python = ">=3.9"
13
+ keywords = ["ukrainian", "declension", "grammatical-cases", "names", "military"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Natural Language :: Ukrainian",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3 :: Only",
22
+ "Topic :: Text Processing :: Linguistic",
23
+ ]
24
+
25
+ [project.urls]
26
+ Upstream = "https://github.com/tooleks/shevchenko-js"
27
+
28
+ [tool.setuptools]
29
+ packages = ["shevchenko"]
30
+
31
+ [tool.setuptools.package-data]
32
+ shevchenko = ["data/*.json", "data/*.bin"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+