Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,37 @@ Models based on Pydantic provide deserialize with basic validation, serialize, a

Allows for easy FastAPI standup.

## TRAPI versions

`translator_tom` provides models for multiple TRAPI versions. When importing directly from `translator_tom`, you automatically import models for the latest version.

```python
from translator_tom import Response # TRAPI 2.0 (latest)
from translator_tom.model_dicts import ResponseDict # TRAPI 2.0 (latest)
```

To pin a specific version, import it from its version subpackage:

```python
from translator_tom.v2_0 import Response # TRAPI 2.0
from translator_tom.v1_6 import Response # TRAPI 1.6
from translator_tom.v1_6.model_dicts import ResponseDict
```

Each version has the same general API: models, model_dicts, diff, semantic validation (WIP). Some items are version-agnostic (Biolink, CURIEs, `TOMBase`, etc.) and shared between the two (`translator_tom.utils`).

### Converting TRAPI versions

The TRAPI 2.0 package provides a utility for converting 1.6 models to 2.0 models:

```python
from translator_tom import up_version

my_v1_response = ... # Some TRAPI 1.6 response

my_v2_response = up_version(my_v1_response)
```

## Model Usage

The main ways you interact with a Model are as follows:
Expand Down Expand Up @@ -344,6 +375,15 @@ This returns a list of warnings and errors with clear descriptions and tuples de
> [!WARNING]
> This feature is WIP and does not do every bit of semantic validation you might expect.

## Scripts

TOM provides some module-level scripts, for your convenience:

- `tom-parse`: Parse a given JSON into a given TOM model to check that it parses.
- `tom-validate`: Run semantic validation (WIP) against a given JSON/TOM model.
- `tom-up-version`: Upgrade a TRAPI 1.6 JSON to TRAPI 2.0.
- `tom-diff`: Diff two JSONs of a given TOM model.

## Design Decisions

There are a view caveats to using TOM, listed below:
Expand Down
23 changes: 12 additions & 11 deletions bench/test_sd.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
"""Quick serdes benchmark: one file per size bucket, comparing TOM vs reasoner-pydantic.

Streams per-file timings in an aligned format as they complete, then prints a
summary table across files at the end.
Runs against `data/example_trapi/<version>/`. Defaults to `1.6` because the pinned
reasoner-pydantic models TRAPI 1.x; pass `--version 2.0` for TOM-only 2.0 timings
(the reasoner-pydantic rows are not meaningful there). Streams per-file timings in an
aligned format as they complete, then prints a summary table across files at the end.
"""

import time

import orjson
from pydantic import TypeAdapter

from utils import CORPUS_ROOT, read_corpus_file
from utils import corpus_root, import_version, parse_version, read_corpus_file

VERSION = parse_version(__doc__, default="1.6")
CORPUS_ROOT = corpus_root(VERSION)

LABEL_WIDTH = 23
VALUE_FMT = "{:>8.4f}s"
Expand Down Expand Up @@ -45,8 +50,7 @@ def section(title: str) -> None:
# --- Imports ---

t0 = time.perf_counter()
from translator_tom import Response # noqa: E402

Response = import_version(VERSION).Response
t_tom = time.perf_counter() - t0

t0 = time.perf_counter()
Expand All @@ -55,7 +59,7 @@ def section(title: str) -> None:
t_rp = time.perf_counter() - t0

section("Imports")
print(f" {'translator_tom':<{LABEL_WIDTH}} {VALUE_FMT.format(t_tom)}")
print(f" {f'translator_tom {VERSION}':<{LABEL_WIDTH}} {VALUE_FMT.format(t_tom)}")
print(f" {'reasoner-pydantic':<{LABEL_WIDTH}} {VALUE_FMT.format(t_rp)}")


Expand Down Expand Up @@ -103,9 +107,7 @@ def section(title: str) -> None:
pair_row("adapter.python", t_vp, t_dp, file_results)

# Combined dict-based pipeline (alternative to adapter.json single-pass).
pair_row(
"orjson + adapter.python", t_loads + t_vp, t_dp + t_dumps, file_results
)
pair_row("orjson + adapter.python", t_loads + t_vp, t_dp + t_dumps, file_results)

# --- adapter (json: bytes <-> model) ---
t0 = time.perf_counter()
Expand Down Expand Up @@ -154,8 +156,7 @@ def section(title: str) -> None:
section("Summary (seconds): from / to")

short_labels = {
lbl: lbl.split("/")[-1].removesuffix(".gz").removesuffix(".json")
for lbl in results
lbl: lbl.split("/")[-1].removesuffix(".gz").removesuffix(".json") for lbl in results
}
ops = list(next(iter(results.values())).keys())

Expand Down
22 changes: 15 additions & 7 deletions bench/test_sd_tom.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
"""TOM-only serdes benchmark across every example response.

Walks `data/example_trapi/**` and runs the core (de)serialization paths for
each file. Streams per-file timings in an aligned format as they complete,
then prints a summary table across files at the end.
Walks `data/example_trapi/<version>/**` (default `2.0`; pass `--version 1.6`) and
runs the core (de)serialization paths for each file. Streams per-file timings in an
aligned format as they complete, then prints a summary table across files at the end.

For a quicker comparison run that also benches reasoner-pydantic on one file
per size bucket, see `bench/test_sd.py`.
"""

import time

from utils import CORPUS_ROOT, discover_files, read_corpus_file
from utils import (
corpus_root,
discover_files,
import_version,
parse_version,
read_corpus_file,
)

VERSION = parse_version(__doc__)
CORPUS_ROOT = corpus_root(VERSION)

LABEL_WIDTH = 10
VALUE_FMT = "{:>8.4f}s"
Expand Down Expand Up @@ -40,12 +49,11 @@ def section(title: str) -> None:
# --- Import ---

t0 = time.perf_counter()
from translator_tom import Response # noqa: E402

Response = import_version(VERSION).Response
t_tom = time.perf_counter() - t0

section("Imports")
print(f" translator_tom Response {VALUE_FMT.format(t_tom)}")
print(f" translator_tom {VERSION} Response {VALUE_FMT.format(t_tom)}")


TEST_FILES = discover_files(CORPUS_ROOT)
Expand Down
30 changes: 20 additions & 10 deletions bench/test_sd_tom_dicts.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"""Dict-util-only serdes benchmark across every example response.

The `model_dicts` twin of `bench/test_sd_tom.py`: same corpus walk and output,
but driving the `*DictUtil` serdes (raw orjson/ormsgpack over the TypedDict form,
no model construction) instead of the `Response` model. Run both to see the cost
the model layer adds over operating on plain dicts.
The `model_dicts` twin of `bench/test_sd_tom.py`: same corpus walk and output
(default `2.0`; pass `--version 1.6`), but driving the `*DictUtil` serdes (raw
orjson/ormsgpack over the TypedDict form, no model construction) instead of the
`Response` model. Run both to see the cost the model layer adds over operating on
plain dicts.

The `+val` rows re-run the `from` path with `validate=True`, adding a pydantic
`TypeAdapter` pass over the parsed data; their `from` timing minus the plain
Expand All @@ -12,7 +13,16 @@

import time

from utils import CORPUS_ROOT, discover_files, read_corpus_file
from utils import (
corpus_root,
discover_files,
import_version,
parse_version,
read_corpus_file,
)

VERSION = parse_version(__doc__)
CORPUS_ROOT = corpus_root(VERSION)

LABEL_WIDTH = 12
VALUE_FMT = "{:>8.4f}s"
Expand Down Expand Up @@ -42,12 +52,13 @@ def section(title: str) -> None:
# --- Import ---

t0 = time.perf_counter()
from translator_tom.model_dicts import ResponseDictUtil # noqa: E402

ResponseDictUtil = import_version(VERSION, "model_dicts").ResponseDictUtil
t_tom = time.perf_counter() - t0

section("Imports")
print(f" model_dicts ResponseDictUtil {VALUE_FMT.format(t_tom)}")
print(
f" translator_tom {VERSION} model_dicts ResponseDictUtil {VALUE_FMT.format(t_tom)}"
)


TEST_FILES = discover_files(CORPUS_ROOT)
Expand Down Expand Up @@ -101,8 +112,7 @@ def section(title: str) -> None:
section("Summary (seconds): from / to")

short_labels = {
lbl: lbl.split("/")[-1].removesuffix(".gz").removesuffix(".json")
for lbl in results
lbl: lbl.split("/")[-1].removesuffix(".gz").removesuffix(".json") for lbl in results
}
ops = list(next(iter(results.values())).keys())

Expand Down
28 changes: 19 additions & 9 deletions bench/test_semantic_validation.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
"""Semantic-validation benchmark across every example response.

Walks `data/example_trapi/**`, deserializes each file, and runs
`semantic_validate` on the resulting `Response`. Streams per-file timings and
the error/warning counts as they complete, then prints a summary table across
files at the end.
Walks `data/example_trapi/<version>/**` (default `2.0`; pass `--version 1.6`),
deserializes each file, and runs `semantic_validate` on the resulting `Response`.
Streams per-file timings and the error/warning counts as they complete, then prints
a summary table across files at the end.

For the serdes benchmarks see `bench/test_sd_tom.py` (TOM-only, every file) and
`bench/test_sd.py` (one file per size bucket, TOM vs reasoner-pydantic).
"""

import time

from utils import CORPUS_ROOT, discover_files, read_corpus_file
from utils import (
corpus_root,
discover_files,
import_version,
parse_version,
read_corpus_file,
)

VERSION = parse_version(__doc__)
CORPUS_ROOT = corpus_root(VERSION)

VALUE_FMT = "{:>8.4f}s"

Expand All @@ -24,13 +33,14 @@ def section(title: str) -> None:
# --- Import ---

t0 = time.perf_counter()
from translator_tom import Response # noqa: E402
from translator_tom.validation import semantic_validate # noqa: E402

_ttom = import_version(VERSION)
_validation = import_version(VERSION, "validation")
t_tom = time.perf_counter() - t0
Response = _ttom.Response
semantic_validate = _validation.semantic_validate

section("Imports")
print(f" translator_tom + validation {VALUE_FMT.format(t_tom)}")
print(f" translator_tom {VERSION} + validation {VALUE_FMT.format(t_tom)}")


TEST_FILES = discover_files(CORPUS_ROOT)
Expand Down
56 changes: 53 additions & 3 deletions bench/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,66 @@

Kept stdlib-only on purpose: the scripts time the `translator_tom` import, so
importing this module must not pull in `translator_tom` (or any heavy dep) and
perturb that measurement.
perturb that measurement. `import_version` imports it lazily, only when a script
calls it inside its timed section.

Every bench takes a ``--version`` arg (``1.6``/``2.0``) selecting both the corpus
directory and the model set, so a bench can run against any supported TRAPI version.
"""

import argparse
import gzip
import importlib
from pathlib import Path
from types import ModuleType

VERSIONS = ("1.6", "2.0") # user-facing TRAPI versions
DEFAULT_VERSION = "2.0"
CORPUS_BASE = Path("data/example_trapi")


def _package(version: str) -> str:
"""Map a user-facing TRAPI version to its package/dir name (e.g. `2.0` -> `v2_0`)."""
return f"v{version.replace('.', '_')}"

CORPUS_ROOT = Path("data/example_trapi")

def parse_version(
description: str | None = None, default: str = DEFAULT_VERSION
) -> str:
"""Parse the shared ``--version`` CLI arg, returning the selected TRAPI version.

``default`` lets a script pin a different default (e.g. `test_sd.py` defaults to
`1.6`, the version its reasoner-pydantic comparand models).
"""
parser = argparse.ArgumentParser(description=description)
parser.add_argument(
"-v",
"--version",
choices=VERSIONS,
default=default,
help=f"TRAPI version to bench: corpus dir + models (default: {default})",
)
return parser.parse_args().version


def corpus_root(version: str) -> Path:
"""The example-corpus directory for `version`."""
return CORPUS_BASE / _package(version)


def import_version(version: str, submodule: str = "") -> ModuleType:
"""Import and return a `translator_tom` version subpackage, or a submodule of it.

Imported lazily (only when a script calls this, inside its timed section) so that
importing `utils` never pulls in `translator_tom`.
"""
name = f"translator_tom.{_package(version)}"
if submodule:
name = f"{name}.{submodule}"
return importlib.import_module(name)


def discover_files(root: Path = CORPUS_ROOT) -> list[Path]:
def discover_files(root: Path) -> list[Path]:
"""Return every `.json` and `.json.gz` under `root`, sorted by bucket size.

Buckets are the immediate-parent directory name (`<N>mb`); we sort by N
Expand Down
10 changes: 5 additions & 5 deletions data/example_trapi/fix_jsons.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@
datetime_now = str(datetime.datetime.now())

files_to_test = [
"./10mb/pathfinder.json",
"./10mb/log-heavy.json",
"./50mb/lookup.json",
"./50mb/result-heavy.json",
"./250mb/attribute-heavy.json.gz",
"./v1_6/10mb/pathfinder.json",
"./v1_6/10mb/log-heavy.json",
"./v1_6/50mb/lookup.json",
"./v1_6/50mb/result-heavy.json",
"./v1_6/250mb/attribute-heavy.json.gz",
]

for response_file_name in files_to_test:
Expand Down
1 change: 1 addition & 0 deletions data/example_trapi/v2_0/10mb/log-heavy.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions data/example_trapi/v2_0/10mb/pathfinder.json

Large diffs are not rendered by default.

Loading
Loading