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.
- shevchenko_py-0.1.0/LICENSE +22 -0
- shevchenko_py-0.1.0/MANIFEST.in +4 -0
- shevchenko_py-0.1.0/PKG-INFO +266 -0
- shevchenko_py-0.1.0/README.md +245 -0
- shevchenko_py-0.1.0/pyproject.toml +32 -0
- shevchenko_py-0.1.0/setup.cfg +4 -0
- shevchenko_py-0.1.0/shevchenko/__init__.py +178 -0
- shevchenko_py-0.1.0/shevchenko/_classifier.py +87 -0
- shevchenko_py-0.1.0/shevchenko/_data.py +20 -0
- shevchenko_py-0.1.0/shevchenko/_gender.py +42 -0
- shevchenko_py-0.1.0/shevchenko/_inflector.py +81 -0
- shevchenko_py-0.1.0/shevchenko/_language.py +53 -0
- shevchenko_py-0.1.0/shevchenko/_military.py +64 -0
- shevchenko_py-0.1.0/shevchenko/_names.py +58 -0
- shevchenko_py-0.1.0/shevchenko/data/classifier-weights.bin +0 -0
- shevchenko_py-0.1.0/shevchenko/data/declension-rules.json +1 -0
- shevchenko_py-0.1.0/shevchenko/data/family-name-classes.json +1 -0
- shevchenko_py-0.1.0/shevchenko/data/given-name-rules.json +1 -0
- shevchenko_py-0.1.0/shevchenko/data/military-hyphenation.json +1 -0
- shevchenko_py-0.1.0/shevchenko/data/military-word-classes.json +1 -0
- shevchenko_py-0.1.0/shevchenko/data/patronymic-name-rules.json +1 -0
- shevchenko_py-0.1.0/shevchenko_py.egg-info/PKG-INFO +266 -0
- shevchenko_py-0.1.0/shevchenko_py.egg-info/SOURCES.txt +23 -0
- shevchenko_py-0.1.0/shevchenko_py.egg-info/dependency_links.txt +1 -0
- shevchenko_py-0.1.0/shevchenko_py.egg-info/top_level.txt +1 -0
|
@@ -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,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"]
|