diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 883406b7..9a64928e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v4.6.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -10,7 +10,7 @@ repos: - id: mixed-line-ending - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.3.2 + rev: v0.4.4 hooks: - id: ruff - id: ruff-format diff --git a/README.md b/README.md index 63abac6b..d9c7ae19 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,13 @@ Key features are of the earthquake detection and localisation framework are: * 1D Layered velocity model * 3D fast-marching velocity model (NonLinLoc compatible) * Extraction of earthquake event features: - * Local magnitudes - * Ground motion attributes + * Moment Magnitudes (MW) based on modelled peak ground motions + * Local magnitudes (ML), different models + * Ground motion attributes (e.g. PGA, PGV, ...) * Automatic extraction of modelled and picked travel times -* Calculation and application of station corrections / station delay times +* Station Corrections + * station specific corrections (SST) + * source specific station corrections (SSST) Qseek is built on top of [Pyrocko](https://pyrocko.org). diff --git a/src/qseek/utils.py b/src/qseek/utils.py index 59502da1..b5f25642 100644 --- a/src/qseek/utils.py +++ b/src/qseek/utils.py @@ -116,7 +116,7 @@ def match(self, other: NSL) -> bool: return self.network == other.network @classmethod - def parse(cls, nsl: str | NSL | list[str]) -> NSL: + def parse(cls, nsl: str | NSL | list[str] | tuple[str, str, str]) -> NSL: """Parse the given NSL string and return an NSL object. Args: @@ -132,22 +132,49 @@ def parse(cls, nsl: str | NSL | list[str]) -> NSL: raise ValueError("invalid empty NSL") if type(nsl) is _NSL: return nsl - if isinstance(nsl, list): + if isinstance(nsl, (list, tuple)): return cls(*nsl) if not isinstance(nsl, str): raise ValueError(f"invalid NSL {nsl}") + parts = nsl.split(".") n_parts = len(parts) if n_parts >= 3: return cls(*parts[:3]) if n_parts == 2: return cls(parts[0], parts[1], "") - if n_parts == 1: - return cls(parts[0], "", "") - raise ValueError(f"invalid NSL {nsl}") + raise ValueError( + f"invalid NSL `{nsl}`, expecting `..`, " + "e.g. `6A.STA130.00`, `6A.STA130` or `.STA130`" + ) + + def _check(self) -> None: + """Check if the current NSL object matches another NSL object. + + Args: + nsl (NSL): The NSL object to compare with. + + Returns: + bool: True if the objects match, False otherwise. + """ + if len(self.network) > 2: + raise ValueError( + f"invalid network {self.network} for {self.pretty}," + " expected 0-2 characters for network code" + ) + if len(self.station) > 5 or len(self.station) < 1: + raise ValueError( + f"invalid station {self.station} for {self.pretty}," + " expected 1-5 characters for station code" + ) + if len(self.location) > 2: + raise ValueError( + f"invalid location {self.location} for {self.pretty}," + " expected 0-2 characters for location code" + ) -NSL = Annotated[_NSL, BeforeValidator(_NSL.parse)] +NSL = Annotated[_NSL, BeforeValidator(_NSL.parse), AfterValidator(_NSL._check)] class _Range(NamedTuple): diff --git a/test/test_utils.py b/test/test_utils.py index 1adff0ec..130bbbfd 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -1,3 +1,4 @@ +import pytest from pydantic import BaseModel from qseek.utils import NSL @@ -23,3 +24,24 @@ class Model(BaseModel): } """ Model.model_validate_json(json) + + json = """ + { + "nsl": "6E.TE234.", + "nsl_list": [".TE232"] + } + """ + Model.model_validate_json(json) + + json_tpl = """ + {{ + "nsl": "{code}", + "nsl_list": [] + }} + """ + + invalid_codes = ["6E", "6E5.", "6E.", "6E.TE123112"] + + for code in invalid_codes: + with pytest.raises(ValueError): + Model.model_validate_json(json_tpl.format(code=code))