Metadata-Version: 2.4
Name: shevchenko-py
Version: 0.5.0
Summary: Zero-dependency Ukrainian declension of personal names, military ranks, and appointments (port of shevchenko-js). Import name: shevchenko.
Author: Misha
License: MIT
Project-URL: Upstream, https://github.com/tooleks/shevchenko-js
Keywords: ukrainian,declension,grammatical-cases,names,military
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Natural Language :: Ukrainian
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Text Processing :: Linguistic
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# shevchenko-py

Ukrainian grammatical-case declension of **personal names**, **military ranks**, and
**military appointments**. A minimal, dependency-free Python port of
[shevchenko-js](https://github.com/tooleks/shevchenko-js) and
[shevchenko-ext-military](https://github.com/tooleks/shevchenko-ext-military).

- Zero runtime dependencies (pure Python ≥ 3.9, stdlib only)
- Tiny: ~44 KB wheel, ~170 KB installed (39 KB code + 130 KB data), ~9 ms import
- All 7 Ukrainian grammatical cases
- **Inverse declension**: recover the nominative from any case
  (`to_nominative`) — a capability the JS original does not have
- Gender auto-detection from the patronymic or given name
- The upstream ML surname classifier reimplemented as ~30 lines of pure Python

## Install

```sh
pip install shevchenko-py
```

The distribution is named `shevchenko-py`; the import name is `shevchenko`.
Don't install this alongside the unrelated [`shevchenko`](https://pypi.org/project/shevchenko/)
PyPI package (a different port, anthroponyms only) — both provide the
`shevchenko` module.

## Quick start

```python
import shevchenko

shevchenko.in_genitive(
    given_name="Тарас",
    patronymic_name="Григорович",
    family_name="Шевченко",
    military_rank="старший солдат",
    military_appointment="заступник командира взводу",
)
# {'given_name': 'Тараса', 'patronymic_name': 'Григоровича',
#  'family_name': 'Шевченка', 'military_rank': 'старшого солдата',
#  'military_appointment': 'заступника командира взводу'}
```

## API reference

Everything is importable from the top-level `shevchenko` package. Anything
prefixed with `_` (modules `_inflector`, `_names`, …) is internal and not a
stable interface.

### Declension functions

```python
shevchenko.in_nominative(input=None, **kwargs) -> dict   # називний  (хто? що?)
shevchenko.in_genitive(input=None, **kwargs) -> dict     # родовий   (кого? чого?)
shevchenko.in_dative(input=None, **kwargs) -> dict       # давальний (кому? чому?)
shevchenko.in_accusative(input=None, **kwargs) -> dict   # знахідний (кого? що?)
shevchenko.in_ablative(input=None, **kwargs) -> dict     # орудний   (ким? чим?)
shevchenko.in_locative(input=None, **kwargs) -> dict     # місцевий  (на кому? на чому?)
shevchenko.in_vocative(input=None, **kwargs) -> dict     # кличний   (звертання)

shevchenko.in_case(case, input=None, **kwargs) -> dict
```

All seven are thin wrappers around `in_case` with the case fixed. Upstream calls
the instrumental case (орудний) "ablative"; this port keeps that naming.

`in_case(case, ...)` takes the case as a string — one of `shevchenko.CASES`
(`"nominative"`, `"genitive"`, `"dative"`, `"accusative"`, `"ablative"`,
`"locative"`, `"vocative"`) — useful when the case is chosen at runtime:

```python
for case in shevchenko.CASES:
    print(shevchenko.in_case(case, gender="masculine", given_name="Тарас"))
```

#### Input

Fields may be passed as a dict, as keyword arguments, or both (keyword
arguments override the dict):

```python
shevchenko.in_vocative({"given_name": "Тарас"}, family_name="Шевченко")
# {'given_name': 'Тарасе', 'family_name': 'Шевченку'}
```

| Field | Meaning | Example |
|---|---|---|
| `given_name` | First name (ім'я) | `"Тарас"` |
| `patronymic_name` | Patronymic (по батькові) | `"Григорович"` |
| `family_name` | Surname (прізвище) | `"Шевченко"` |
| `military_rank` | Rank phrase (військове звання) | `"старший солдат"` |
| `military_appointment` | Appointment/position phrase (посада) | `"заступник командира взводу"` |
| `gender` | `"masculine"` / `"feminine"`; optional, see below | `"masculine"` |

All fields are optional strings, but at least one non-`gender` field is
required. Unknown keys raise `InputValidationError` (so `givenName=` typos are
caught, not silently ignored). Values are NFC-normalized before matching.

#### The `gender` parameter

`gender` applies to the three name fields. If omitted, it is auto-detected from
the patronymic (preferred) or the given name — same logic as
[`detect_gender`](#detect_gender). Two consequences:

- Input with a patronymic or a recognizable given name never needs `gender`.
- A lone `family_name` usually can't be sexed — pass `gender` explicitly or
  `InputValidationError` is raised.

Military fields don't need `gender` at all: each word inside a rank or
appointment phrase carries its own grammatical gender (`медична сестра`
declines as feminine regardless of the person).

#### Return value

A dict containing **only the fields you passed in** (never `gender`), each
value inflected in the requested case. Field order is fixed: `given_name`,
`patronymic_name`, `family_name`, `military_rank`, `military_appointment`.

#### Behavior notes

- **Letter case is preserved**: `ШЕВЧЕНКО` → `ШЕВЧЕНКА`, `Шевченко` → `Шевченка`.
- **Hyphenated names** decline part by part (`Нечуй-Левицький` →
  `Нечуя-Левицького`); monosyllabic non-final surname parts stay frozen
  (`Драй-Хмара` → `Драй-Хмари`).
- **Unknown words in military phrases** — proper names, abbreviations,
  qualifiers already in genitive — pass through unchanged:

  ```python
  shevchenko.in_ablative(military_appointment='оператор БПЛА "Фурія"')
  # {'military_appointment': 'оператором БПЛА "Фурія"'}
  ```
- **Unknown names** that no declension rule matches are returned unchanged
  rather than guessed at.

#### Errors

`InputValidationError` (subclass of `ValueError`) is raised when:

- `case` is not one of `CASES` (for `in_case`),
- no field besides `gender` is provided,
- a field value is not a string,
- an unknown parameter is passed,
- `gender` is neither `"masculine"`, `"feminine"`, nor omitted,
- `gender` is omitted for name fields and cannot be auto-detected.

### `detect_gender`

```python
shevchenko.detect_gender(input=None, **kwargs) -> str | None
```

Detects the grammatical gender from `patronymic_name` (checked first) or
`given_name` endings. Accepts `family_name` too, but a surname alone is never
used for detection. Returns `"masculine"`, `"feminine"`, or `None` when
undetectable — unlike the declension functions it does not raise on ambiguity.

```python
shevchenko.detect_gender(patronymic_name="Григорович")   # 'masculine'
shevchenko.detect_gender(given_name="Оксана")            # 'feminine'
shevchenko.detect_gender(family_name="Шевченко")         # None
```

Input rules are the same as for declension: dict and/or kwargs, at least one
field, strings only, unknown keys raise `InputValidationError`.

### `to_nominative`

```python
shevchenko.to_nominative(input=None, case=None, **kwargs) -> dict
```

The inverse of the declension functions: recovers nominative forms from fields
written in **any** grammatical case. Same five fields as `in_case`, dict
and/or kwargs.

```python
shevchenko.to_nominative(
    given_name="Тараса", patronymic_name="Григоровича", family_name="Шевченка",
    military_rank="старшого солдата",
    military_appointment="заступника командира взводу",
)
# {'given_name': 'Тарас', 'patronymic_name': 'Григорович',
#  'family_name': 'Шевченко', 'military_rank': 'старший солдат',
#  'military_appointment': 'заступник командира взводу',
#  'gender': 'masculine', 'alternatives': {...}}
```

How it works: candidates are generated by mechanically inverting the forward
declension rules, then each is verified by declining it forward — every
returned value round-trips exactly. Since distinct nominatives can share an
oblique form (Сірка ← Сірк or Сірко), the result carries deterministic
ranking metadata:

- `"gender"` — resolved from the patronymic suffix (works in every case), an
  explicit `gender=` argument, or both-gender agreement; raises
  `InputValidationError` when unresolvable.
- `"alternatives"` — per-field list of other valid readings, present only for
  genuinely ambiguous fields. **The true nominative is always either the
  returned value or in this list** (100% recall on the full upstream corpora).

Behavior notes:

- All name fields are assumed to share one case; unambiguous fields
  (patronymics invert deterministically) pin the case for ambiguous ones.
  Pass `case=` when the source case is known — it narrows ambiguity further.
- Mixed input is handled: `to_nominative(family_name="Іванов",
  given_name="Івана")` returns both in nominative.
- Already-nominative input comes back unchanged.
- In military phrases only the leading adjectives and head noun invert;
  genitive attributes after the head stay put («заступника командира взводу» →
  «заступник командира взводу»).
- Vocative is excluded from the default search (it never occurs in documents
  and collides with nominative -о forms); pass `case="vocative"` explicitly.

Measured on the upstream corpora: patronymics and military ranks invert at
100% top-1; given names ≈99%, family names ≈93% top-1 (the remaining gap is
orthography-codified ambiguity — Верес/Вересов, Сашко/Сашок, Тичин/Тичина —
reported via `alternatives`); military appointments 96.7% top-1 / 99.3% with
alternatives. When neither the patronymic nor an explicit `gender` decides,
the given name is inverted against the bundled dictionary of 1,144 real names
(«Івана» → Іван → masculine) before an error is raised.
Linguistic grounding, sources, and design details:
[docs/inverse-declension.md](docs/inverse-declension.md).

### Constants

| Constant | Value |
|---|---|
| `shevchenko.CASES` | `("nominative", "genitive", "dative", "accusative", "ablative", "locative", "vocative")` |
| `shevchenko.NOMINATIVE` … `shevchenko.VOCATIVE` | The individual case strings |
| `shevchenko.GENDERS` | `("masculine", "feminine")` |
| `shevchenko.MASCULINE`, `shevchenko.FEMININE` | The individual gender strings |
| `shevchenko.__version__` | Package version string |

The constants are plain strings/tuples, so `gender="feminine"` and
`gender=shevchenko.FEMININE` are interchangeable.

### `InputValidationError`

```python
class InputValidationError(ValueError)
```

Raised by all public functions on invalid input; see [Errors](#errors).

## Performance

Measured on Python 3.14 with `python benchmark.py` (CPython, Windows, single
thread):

| Operation | Speed |
|---|---|
| Cold import (data load; regexes compile lazily) | ~8 ms, once |
| Full person (3 names + rank + appointment), 1 case | ~140 µs (≈7,000/s) |
| Single name field | ~20–24 µs (≈42–50,000/s) |
| Military rank / appointment phrase | ~40–50 µs (≈21–25,000/s) |
| `detect_gender` | ~1 µs (≈700,000/s) |
| Ambiguous surname, first time (pure-Python RNN) | ~0.9 ms |
| Ambiguous surname, repeated (memoized) | ~10 µs |
| Real-corpus throughput (name triple, one case) | ≈13,000/s |
| `to_nominative`: full person, first time | ~3.3 ms |
| `to_nominative`: full person, repeated names | ~0.7 ms (~0.3 ms with `case=`) |
| `to_nominative`: military rank / appointment, repeated | ~70–100 µs |
| `to_nominative`: patronymic only | ~85 µs |

The only slow path is the first classification of an ambiguous surname (the
neural network runs in pure Python); results are memoized, so each unique
surname pays it once per process.

## Fidelity

The suite (2,383 tests) covers:

- **Forward declension** against the complete upstream corpora: every
  anthroponym test case, every military rank and appointment in all 7 cases,
  and the 1,144-name gender detection dataset. Also differentially verified
  against the live JS library on 3,990 additional word/case combinations —
  zero mismatches.
- **Inverse declension** against the same corpora, both as aggregate accuracy
  floors and as the round-trip property
  `in_case(case, to_nominative(x)) == x`. The recall invariant — the true
  nominative is never absent from result + alternatives — is asserted over
  every corpus form.
- All upstream functional unit tests (letter case, syllables, alphabet
  encoding, classifier input encoding and decode threshold) are ported;
  upstream's input-validation suite is not, since this port's API surface
  deliberately differs.
- Data integrity: every regex in the shipped data must compile under
  Python's `re` (the fail-fast guard for upstream rule re-syncs).

## How it works

Declension is rule-based: 99 regex rules from upstream (`shevchenko/data/`)
keyed by gender, word class, and usage, applied by priority. Surnames whose word
class is ambiguous (e.g. feminine `-а/-я`, masculine `-ий/-ой/-их`) are
classified noun-vs-adjective by the upstream neural network — an
Embedding→SimpleRNN(16)→Dense model small enough (4 KB) to run in pure Python
(`shevchenko/_classifier.py`). Two upstream regexes use variable-width
lookbehinds unsupported by Python's `re` and are rewritten to equivalent forms at
load time (`shevchenko/_inflector.py`).

The runtime data is regenerated from the upstream sources by
`tools/build_data.py` (dev-only): it strips documentation-only fields, drops
empty case entries, minifies, and verifies every pattern compiles under
Python's `re`. Data loading uses plain file reads instead of
`importlib.resources`, whose import chain alone costs ~25 ms.

## Differences from shevchenko-js

| | shevchenko-js | shevchenko-py |
|---|---|---|
| Field names | `givenName`, `familyName`, … | `given_name`, `family_name`, … |
| Military fields | separate extension, `registerExtension(...)` | built in |
| `gender` | always required | auto-detected when omitted |
| API style | `async`/`await` | plain synchronous calls |
| Validation errors | `InputValidationError extends TypeError` | `InputValidationError(ValueError)` |
| Inverse (case → nominative) | not available | `to_nominative` |

## License

MIT. Declension rules, gender rules, model weights, and test corpora are from
shevchenko-js © Oleksandr Tolochko et al., MIT-licensed.
