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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout your repository using git
uses: actions/checkout@v6
uses: actions/checkout@v7

- name: Install, build, and upload your site
uses: withastro/action@v5
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ jobs:
ruff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- uses: chartboost/ruff-action@v1
with:
src: './pyenzyme'
Expand Down
20 changes: 17 additions & 3 deletions .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,28 @@ on:
types: [released]

jobs:
deploy:
check-version:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Verify release tag matches package version
run: |
TAG="${GITHUB_REF_NAME#v}"
VER="$(grep -m1 '^version = ' pyproject.toml | sed -E 's/.*"([^"]+)".*/\1/')"
echo "tag=$TAG pyproject=$VER"
test "$TAG" = "$VER"

test:
uses: ./.github/workflows/unit-tests.yaml

publish:
needs: [check-version, test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v7

- name: Install uv
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v9.0.0

- name: Publish Package
env:
Expand Down
30 changes: 0 additions & 30 deletions .github/workflows/remote-tests.yaml

This file was deleted.

6 changes: 3 additions & 3 deletions .github/workflows/test-publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v7

- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v7
with:
python-version: '3.11'

- name: Install uv
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v9.0.0

- name: Publish Package
env:
Expand Down
12 changes: 7 additions & 5 deletions .github/workflows/unit-tests.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
name: Unit Tests

on: [push]
on:
push:
workflow_call:

jobs:
build:
Expand All @@ -9,17 +11,17 @@ jobs:
max-parallel: 4
fail-fast: false
matrix:
python-version: ['3.11', '3.12', '3.13']
python-version: ['3.11', '3.12', '3.13', '3.14']

steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python-version }}
- name: Install uv
uses: astral-sh/setup-uv@v7
uses: astral-sh/setup-uv@v9.0.0
- name: Install the project
run: uv sync --locked --all-extras --all-groups
- name: Run tests
Expand Down
14 changes: 14 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Run once per clone: uv run pre-commit install
repos:
- repo: https://github.com/astral-sh/uv-pre-commit
rev: 0.11.31
hooks:
# Re-locks uv.lock whenever pyproject.toml changes, so the two can't drift.
- id: uv-lock

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.22
hooks:
# Same lint gate CI runs (lint.yml lints ./pyenzyme and ./tests only).
- id: ruff-check
files: ^(pyenzyme|tests)/
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,13 @@ For new projects, we recommend utilizing the updated API available in the packag

## 🧪 Testing

Contributors should install the pre-commit hooks once per clone (keeps `uv.lock` in sync and runs the linter before each commit):

```bash
uv tool install pre-commit
pre-commit install
```

In order to run tests there are two different ways. First you can utilize `pytest` directly by running the following:

```bash
Expand Down
4 changes: 3 additions & 1 deletion pyenzyme/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

from importlib.metadata import version

from mdmodels.units.unit_definition import UnitDefinition, UnitType

from .composer import compose
Expand Down Expand Up @@ -45,4 +47,4 @@
"group_measurements",
]

__version__ = "2.2.2"
__version__ = version("pyenzyme")
10 changes: 6 additions & 4 deletions pyenzyme/composer.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,16 +197,18 @@ def _fetch_with_fetchers(
Raises:
ValueError: If no fetcher can handle the given entity ID
"""
errors = []
for fetcher in fetchers:
try:
return fetcher(entity_id)
except Exception:
except Exception as e:
errors.append(f"{fetcher.__name__}: {type(e).__name__}: {e}")
continue

fetcher_names = ", ".join(f.__name__ for f in fetchers)
detail = "; ".join(errors)
raise ValueError(
f"No {entity_type} fetcher found for {entity_id}. "
f"Supported fetchers: {fetcher_names}"
f"No {entity_type} fetcher succeeded for {entity_id}. "
f"Tried: {detail}"
)


Expand Down
9 changes: 4 additions & 5 deletions pyenzyme/fetcher/pdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class Citation(BaseModel):
journal_name: Optional[str] = Field(default=None, alias="journal_abbrev")
year: Optional[int] = None
doi: Optional[str] = None
pubmed_id: Optional[str] = Field(default=None, alias="pdbx_database_id_PubMed")
pubmed_id: Optional[int] = Field(default=None, alias="pdbx_database_id_PubMed")


class StructInfo(BaseModel):
Expand Down Expand Up @@ -261,9 +261,8 @@ def fetch_pdb(
if pdb_response.citation and pdb_response.citation[0].doi:
protein.references.append(f"https://doi.org/{pdb_response.citation[0].doi}")

if pdb_response.citation and pdb_response.citation[0].pubmed_id:
protein.references.append(
f"https://pubmed.ncbi.nlm.nih.gov/{pdb_response.citation[0].pubmed_id}"
)
if pdb_response.citation and pdb_response.citation[0].pubmed_id is not None:
pubmed_id = str(pdb_response.citation[0].pubmed_id)
protein.references.append(f"https://pubmed.ncbi.nlm.nih.gov/{pubmed_id}")

return protein
17 changes: 9 additions & 8 deletions pyenzyme/sbml/omex.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,14 +114,15 @@ def create_sbml_omex(
sbml_path = f"{temp_dir}/model.xml"
with open(sbml_path, "w") as f:
f.write(sbml_doc)
omex.add_entry(
entry_path=Path(sbml_path),
entry=ManifestEntry(
location="./model.xml",
format=EntryFormat.SBML,
master=True,
),
)
# add_entry copies from entry_path, so the file must be closed/flushed first
omex.add_entry(
entry_path=Path(sbml_path),
entry=ManifestEntry(
location="./model.xml",
format=EntryFormat.SBML,
master=True,
),
)

if data is not None:
data_path = f"{temp_dir}/data.tsv"
Expand Down
7 changes: 5 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "pyenzyme"
version = "2.2.2"
version = "2.2.3"
description = "A Python library for EnzymeML"
authors = [{ name = "Jan Range", email = "range.jan@web.de" }]
requires-python = ">=3.11,<4"
Expand All @@ -10,11 +10,11 @@ dependencies = [
"python-libsbml>=5.20.2,<6",
"rich>=13.7.1,<14",
"pandas>=2.0.0",
"pydantic>=2,<3",
"pydantic-xml>=2.17.0,<3",
"loguru>=0.7.2,<0.8",
"rdflib==7.0.0",
"sympy>=1.12.1,<2",
"fastobo>=0.13.0,<0.14",
"pymetadata>=0.5.3,<0.6",
"httpx>=0.27",
"mdmodels>=0.2.4,<0.3",
Expand Down Expand Up @@ -44,6 +44,9 @@ v1 = [
]
copasi = ["copasi-basico>=0.85"]

[tool.pytest.ini_options]
markers = ["remote: tests that require network access to external services"]

[tool.uv]

[build-system]
Expand Down
3 changes: 2 additions & 1 deletion tests/fixtures/compose/expected_compose.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@
"sequence": "AQYEDGKQYTTLEKPVAGAPQVLEFFSFFCPHCYQFEEVLHISDNVKKKLPEGVKMTKYHVNFMGGDLGKDLTQAWAVAMALGVEDKVTVPLFEGVQKTQTIRSASDIRDVFINAGIKGEEYDAAWNSFVVKSLVAQQEKAAADVQLRGVPAMFVNGKYQLNPQGMDTSNMDVFVQQYADTVKYLSEKK",
"vessel_id": "vessel",
"references": [
"https://www.rcsb.org/structure/1A23"
"https://www.rcsb.org/structure/1A23",
"https://pubmed.ncbi.nlm.nih.gov/9572841"
]
}
],
Expand Down
3 changes: 2 additions & 1 deletion tests/fixtures/compose/expected_compose_no_vessel.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@
"constant": true,
"sequence": "AQYEDGKQYTTLEKPVAGAPQVLEFFSFFCPHCYQFEEVLHISDNVKKKLPEGVKMTKYHVNFMGGDLGKDLTQAWAVAMALGVEDKVTVPLFEGVQKTQTIRSASDIRDVFINAGIKGEEYDAAWNSFVVKSLVAQQEKAAADVQLRGVPAMFVNGKYQLNPQGMDTSNMDVFVQQYADTVKYLSEKK",
"references": [
"https://www.rcsb.org/structure/1A23"
"https://www.rcsb.org/structure/1A23",
"https://pubmed.ncbi.nlm.nih.gov/9572841"
]
}
],
Expand Down
1 change: 1 addition & 0 deletions tests/integration/test_composer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from pyenzyme.tools import to_dict_wo_json_ld


@pytest.mark.remote
class TestComposer:
def test_compose(self):
# Act
Expand Down
30 changes: 30 additions & 0 deletions tests/unit/test_composer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import pytest

from pyenzyme.composer import _fetch_with_fetchers


def test_returns_first_successful_fetcher():
def boom(_):
raise ConnectionError("down")

def ok(entity_id):
return f"fetched:{entity_id}"

assert _fetch_with_fetchers("X1", [boom, ok], "protein") == "fetched:X1"


def test_error_aggregates_every_fetcher_reason():
def bad_request(_):
raise ValueError("400 Bad Request")

def not_found(_):
raise KeyError("404")

with pytest.raises(ValueError) as exc:
_fetch_with_fetchers("X1", [bad_request, not_found], "protein")

msg = str(exc.value)
# names the entity, the type, and each fetcher's real failure reason
assert "No protein fetcher succeeded for X1" in msg
assert "bad_request: ValueError: 400 Bad Request" in msg
assert "not_found: KeyError" in msg
Loading