diff --git a/docs/pages/api-reference.md b/docs/pages/api-reference.md index dbf1a20..67148ef 100644 --- a/docs/pages/api-reference.md +++ b/docs/pages/api-reference.md @@ -79,7 +79,7 @@ Datastructures not in the standard library. Implements: `RingList`, `merge_dicts` -## `RingList` Objects +### `RingList` Objects ```python class RingList() @@ -176,7 +176,7 @@ Decorators that can be used wrap functions. Implements: `with_filelock` -## `with_filelock` Objects +### `with_filelock` Objects ```python class with_filelock() @@ -216,12 +216,12 @@ Functions for interacting with EM27 interferograms. Implements: `detect_corrupt_opus_files`, `load_proffast2_result`. -This requires you to install this utils library with the optional `polars` dependency: +This requires you to install this utils library with the optional `em27` dependency: ```bash -pip install "tum_esm_utils[polars]" +pip install "tum_esm_utils[em27]" ## `or` -pdm add "tum_esm_utils[polars]" +pdm add "tum_esm_utils[em27]" ``` @@ -353,6 +353,60 @@ Implements: `load_file`, `dump_file`, `load_json_file`, `expect_file_contents`, `render_directory_tree`, `list_directory` +##### `load_file` + +```python +def load_file(path: str) -> str +``` + +Load the content of a file. + + +##### `dump_file` + +```python +def dump_file(path: str, content: str) -> None +``` + +Dump content to a file. + + +##### `load_binary_file` + +```python +def load_binary_file(path: str) -> bytes +``` + +Load binary content of a file. + + +##### `dump_binary_file` + +```python +def dump_binary_file(path: str, content: bytes) -> None +``` + +Dump binary content to a file. + + +##### `load_json_file` + +```python +def load_json_file(path: str) -> Any +``` + +Load the content of a JSON file. + + +##### `dump_json_file` + +```python +def dump_json_file(path: str, content: Any, indent: Optional[int] = 4) -> None +``` + +Dump content to a JSON file. + + ##### `get_parent_dir_path` ```python @@ -641,11 +695,14 @@ Add a subplot to a figure. ```python def add_colorpatch_legend(fig: plt.Figure, - handles: list[tuple[str, Union[ + handles: list[tuple[ str, - tuple[float, float, float], - tuple[float, float, float, float], - ]]], + Union[ + str, + tuple[float, float, float], + tuple[float, float, float, float], + ], + ]], ncols: Optional[int] = None, location: str = "upper left") -> None ``` @@ -740,7 +797,7 @@ Implements: `run_shell_command`, `CommandLineException`, `get_hostname`, `get_commit_sha`, `change_file_permissions` -## `CommandLineException` Objects +### `CommandLineException` Objects ```python class CommandLineException(Exception) @@ -1040,7 +1097,7 @@ or "hello world" -> "hello world"). The string with duplicate characters replaced. -## `RandomLabelGenerator` Objects +### `RandomLabelGenerator` Objects ```python class RandomLabelGenerator() @@ -1280,7 +1337,7 @@ spans overlap at a single date. not overlap. -## `ExponentialBackoff` Objects +### `ExponentialBackoff` Objects ```python class ExponentialBackoff() @@ -1353,7 +1410,7 @@ Implements validator utils for use with pydantic models. Implements: `StrictFilePath`, `StrictDirectoryPath` -## `StrictFilePath` Objects +### `StrictFilePath` Objects ```python class StrictFilePath(pydantic.RootModel[str]) @@ -1380,7 +1437,7 @@ m = MyModel.model_validate( ``` -## `StrictDirectoryPath` Objects +### `StrictDirectoryPath` Objects ```python class StrictDirectoryPath(pydantic.RootModel[str]) @@ -1407,7 +1464,7 @@ m = MyModel.model_validate( ``` -## `Version` Objects +### `Version` Objects ```python class Version(pydantic.RootModel[str]) @@ -1434,7 +1491,7 @@ def as_identifier() -> str Return the version string as a number, i.e. MAJOR.MINOR.PATCH... -## `StricterBaseModel` Objects +### `StricterBaseModel` Objects ```python class StricterBaseModel(pydantic.BaseModel) @@ -1444,7 +1501,7 @@ The same as pydantic.BaseModel, but with stricter rules. It does not allow extra fields and validates assignments after initialization. -## `StrictIPv4Adress` Objects +### `StrictIPv4Adress` Objects ```python class StrictIPv4Adress(pydantic.RootModel[str]) @@ -1462,3 +1519,63 @@ m = MyModel(ip='192.186.2.1') m = MyModel(ip='192.186.2.1:22') ``` + +## `tum_esm_utils.opus` + +Functions for interacting with OPUS files. + +Implements: `OpusFile`. + +Read https://tccon-wiki.caltech.edu/Main/I2SAndOPUSHeaders for more information +about the file parameters. This requires you to install this utils library with +the optional `opus` dependency: + +```bash +pip install "tum_esm_utils[opus]" +## `or` +pdm add "tum_esm_utils[opus]" +``` + +Credits to Friedrich Klappenbach (ge79wul@mytum.de) for decoding the OPUS file +format. + + +### `OpusFile` Objects + +```python +class OpusFile(pydantic.BaseModel) +``` + + +##### `read` + +```python +@staticmethod +def read(filepath: str, + measurement_timestamp_mode: Literal["start", "end"] = "start", + interferogram_mode: Literal["skip", "validate", "read"] = "read", + read_all_channels: bool = True) -> OpusFile +``` + +Read an interferogram file. + +**Arguments**: + +- `filepath` - Path to the OPUS file. +- `measurement_timestamp_mode` - Whether the timestamps in the interferograms + indicate the start or end of the measurement +- `interferogram_mode` - How to handle the interferogram data. "skip" + will not read the interferogram data, "validate" + will read the first and last block to check + for errors during writing, "read" will read + the entire interferogram. "read" takes about + 11-12 times longer than "skip", "validate" is + about 20% slower than "skip". +- `read_all_channels` - Whether to read all channels in the file or + only the first one. + + +**Returns**: + + An OpusFile object, optionally containing the interferogram data (in read mode) + diff --git a/docs/scripts/sync-docs.py b/docs/scripts/sync-docs.py index 1154b9e..ebb681c 100644 --- a/docs/scripts/sync-docs.py +++ b/docs/scripts/sync-docs.py @@ -2,9 +2,7 @@ import os import tempfile -PROJECT_DIR = os.path.dirname( - os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -) +PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) INDEX_SRC = os.path.join(PROJECT_DIR, "README.md") INDEX_DST = os.path.join(PROJECT_DIR, "docs", "pages", "index.md") API_DST = os.path.join(PROJECT_DIR, "docs", "pages", "api-reference.md") @@ -20,11 +18,14 @@ # generate automatic API reference and prettify output module_names = list( - sorted([ - f[:-3] for f in os.listdir(os.path.join(PROJECT_DIR, "tum_esm_utils")) - if (f.endswith(".py") and (f != "__init__.py")) - ]) -) + sorted( + [ + f[:-3] + for f in os.listdir(os.path.join(PROJECT_DIR, "tum_esm_utils")) + if (f.endswith(".py") and (f != "__init__.py")) + ] + ) +) + ["opus"] print("Module names:", module_names) parsed_modules = ["--module=tum_esm_utils"] @@ -32,10 +33,7 @@ parsed_modules.append(f"--module=tum_esm_utils.{m}") with tempfile.NamedTemporaryFile() as f: - os.system( - f"cd {PROJECT_DIR} && pydoc-markdown " + (" ").join(parsed_modules) + - f" > {f.name}" - ) + os.system(f"cd {PROJECT_DIR} && pydoc-markdown " + (" ").join(parsed_modules) + f" > {f.name}") with open(f.name, "r") as f2: raw_api_reference_content = f2.read() @@ -48,23 +46,19 @@ assert line_segments[0] == "#" * len(line_segments[0]) if len(line_segments) == 2: parsed_api_reference_content_lines.append( - line_segments[0] + "# `" + - line_segments[1].replace("\\_", "_") + "`" + line_segments[0] + "# `" + line_segments[1].replace("\\_", "_") + "`" ) elif len(line_segments) == 3: assert line_segments[2] == "Objects" parsed_api_reference_content_lines.append( - line_segments[0] + " `" + - line_segments[1].replace("\\_", "_") + "` Objects" + line_segments[0] + "# `" + line_segments[1].replace("\\_", "_") + "` Objects" ) else: raise ValueError("Unexpected line format: " + line) else: parsed_api_reference_content_lines.append(line) - parsed_api_reference_content = "\n".join( - parsed_api_reference_content_lines[2 :] - ) + parsed_api_reference_content = "\n".join(parsed_api_reference_content_lines[2:]) with open(API_DST, "w") as f: f.write("# API Reference \n\n" + parsed_api_reference_content) diff --git a/pdm.lock b/pdm.lock index 757c835..c4a1c6a 100644 --- a/pdm.lock +++ b/pdm.lock @@ -2,10 +2,10 @@ # It is not intended for manual editing. [metadata] -groups = ["default", "all", "dev"] +groups = ["default", "all", "dev", "em27", "opus", "plotting", "polars"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:ea16bd26c28b51c06a0bd9b8fd5ced039096673d69a561de045d2804ccbdb421" +content_hash = "sha256:1f5b018ba27872dfcf156444d6fb816906c035b13dd17e4056b0f8f7898ff61b" [[metadata.targets]] requires_python = "~=3.10" @@ -170,7 +170,7 @@ name = "contourpy" version = "1.3.1" requires_python = ">=3.10" summary = "Python library for calculating contours of 2D quadrilateral grids" -groups = ["all"] +groups = ["all", "plotting"] dependencies = [ "numpy>=1.23", ] @@ -236,7 +236,7 @@ name = "cycler" version = "0.12.1" requires_python = ">=3.8" summary = "Composable style cycles" -groups = ["all"] +groups = ["all", "plotting"] files = [ {file = "cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30"}, {file = "cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"}, @@ -370,45 +370,45 @@ files = [ [[package]] name = "fonttools" -version = "4.55.0" +version = "4.55.2" requires_python = ">=3.8" summary = "Tools to manipulate font files" -groups = ["all"] +groups = ["all", "plotting"] files = [ - {file = "fonttools-4.55.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:51c029d4c0608a21a3d3d169dfc3fb776fde38f00b35ca11fdab63ba10a16f61"}, - {file = "fonttools-4.55.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bca35b4e411362feab28e576ea10f11268b1aeed883b9f22ed05675b1e06ac69"}, - {file = "fonttools-4.55.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ce4ba6981e10f7e0ccff6348e9775ce25ffadbee70c9fd1a3737e3e9f5fa74f"}, - {file = "fonttools-4.55.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31d00f9852a6051dac23294a4cf2df80ced85d1d173a61ba90a3d8f5abc63c60"}, - {file = "fonttools-4.55.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e198e494ca6e11f254bac37a680473a311a88cd40e58f9cc4dc4911dfb686ec6"}, - {file = "fonttools-4.55.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7208856f61770895e79732e1dcbe49d77bd5783adf73ae35f87fcc267df9db81"}, - {file = "fonttools-4.55.0-cp310-cp310-win32.whl", hash = "sha256:e7e6a352ff9e46e8ef8a3b1fe2c4478f8a553e1b5a479f2e899f9dc5f2055880"}, - {file = "fonttools-4.55.0-cp310-cp310-win_amd64.whl", hash = "sha256:636caaeefe586d7c84b5ee0734c1a5ab2dae619dc21c5cf336f304ddb8f6001b"}, - {file = "fonttools-4.55.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fa34aa175c91477485c44ddfbb51827d470011e558dfd5c7309eb31bef19ec51"}, - {file = "fonttools-4.55.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:37dbb3fdc2ef7302d3199fb12468481cbebaee849e4b04bc55b77c24e3c49189"}, - {file = "fonttools-4.55.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5263d8e7ef3c0ae87fbce7f3ec2f546dc898d44a337e95695af2cd5ea21a967"}, - {file = "fonttools-4.55.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f307f6b5bf9e86891213b293e538d292cd1677e06d9faaa4bf9c086ad5f132f6"}, - {file = "fonttools-4.55.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f0a4b52238e7b54f998d6a56b46a2c56b59c74d4f8a6747fb9d4042190f37cd3"}, - {file = "fonttools-4.55.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3e569711464f777a5d4ef522e781dc33f8095ab5efd7548958b36079a9f2f88c"}, - {file = "fonttools-4.55.0-cp311-cp311-win32.whl", hash = "sha256:2b3ab90ec0f7b76c983950ac601b58949f47aca14c3f21eed858b38d7ec42b05"}, - {file = "fonttools-4.55.0-cp311-cp311-win_amd64.whl", hash = "sha256:aa046f6a63bb2ad521004b2769095d4c9480c02c1efa7d7796b37826508980b6"}, - {file = "fonttools-4.55.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:838d2d8870f84fc785528a692e724f2379d5abd3fc9dad4d32f91cf99b41e4a7"}, - {file = "fonttools-4.55.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f46b863d74bab7bb0d395f3b68d3f52a03444964e67ce5c43ce43a75efce9246"}, - {file = "fonttools-4.55.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33b52a9cfe4e658e21b1f669f7309b4067910321757fec53802ca8f6eae96a5a"}, - {file = "fonttools-4.55.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:732a9a63d6ea4a81b1b25a1f2e5e143761b40c2e1b79bb2b68e4893f45139a40"}, - {file = "fonttools-4.55.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7dd91ac3fcb4c491bb4763b820bcab6c41c784111c24172616f02f4bc227c17d"}, - {file = "fonttools-4.55.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1f0e115281a32ff532118aa851ef497a1b7cda617f4621c1cdf81ace3e36fb0c"}, - {file = "fonttools-4.55.0-cp312-cp312-win32.whl", hash = "sha256:6c99b5205844f48a05cb58d4a8110a44d3038c67ed1d79eb733c4953c628b0f6"}, - {file = "fonttools-4.55.0-cp312-cp312-win_amd64.whl", hash = "sha256:f8c8c76037d05652510ae45be1cd8fb5dd2fd9afec92a25374ac82255993d57c"}, - {file = "fonttools-4.55.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8118dc571921dc9e4b288d9cb423ceaf886d195a2e5329cc427df82bba872cd9"}, - {file = "fonttools-4.55.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01124f2ca6c29fad4132d930da69158d3f49b2350e4a779e1efbe0e82bd63f6c"}, - {file = "fonttools-4.55.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81ffd58d2691f11f7c8438796e9f21c374828805d33e83ff4b76e4635633674c"}, - {file = "fonttools-4.55.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5435e5f1eb893c35c2bc2b9cd3c9596b0fcb0a59e7a14121562986dd4c47b8dd"}, - {file = "fonttools-4.55.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d12081729280c39d001edd0f4f06d696014c26e6e9a0a55488fabc37c28945e4"}, - {file = "fonttools-4.55.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a7ad1f1b98ab6cb927ab924a38a8649f1ffd7525c75fe5b594f5dab17af70e18"}, - {file = "fonttools-4.55.0-cp313-cp313-win32.whl", hash = "sha256:abe62987c37630dca69a104266277216de1023cf570c1643bb3a19a9509e7a1b"}, - {file = "fonttools-4.55.0-cp313-cp313-win_amd64.whl", hash = "sha256:2863555ba90b573e4201feaf87a7e71ca3b97c05aa4d63548a4b69ea16c9e998"}, - {file = "fonttools-4.55.0-py3-none-any.whl", hash = "sha256:12db5888cd4dd3fcc9f0ee60c6edd3c7e1fd44b7dd0f31381ea03df68f8a153f"}, - {file = "fonttools-4.55.0.tar.gz", hash = "sha256:7636acc6ab733572d5e7eec922b254ead611f1cdad17be3f0be7418e8bfaca71"}, + {file = "fonttools-4.55.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:bef0f8603834643b1a6419d57902f18e7d950ec1a998fb70410635c598dc1a1e"}, + {file = "fonttools-4.55.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:944228b86d472612d3b48bcc83b31c25c2271e63fdc74539adfcfa7a96d487fb"}, + {file = "fonttools-4.55.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f0e55f5da594b85f269cfbecd2f6bd3e07d0abba68870bc3f34854de4fa4678"}, + {file = "fonttools-4.55.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b1a6e576db0c83c1b91925bf1363478c4bb968dbe8433147332fb5782ce6190"}, + {file = "fonttools-4.55.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:616368b15716781bc84df5c2191dc0540137aaef56c2771eb4b89b90933f347a"}, + {file = "fonttools-4.55.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7bbae4f3915225c2c37670da68e2bf18a21206060ad31dfb95fec91ef641caa7"}, + {file = "fonttools-4.55.2-cp310-cp310-win32.whl", hash = "sha256:8b02b10648d69d67a7eb055f4d3eedf4a85deb22fb7a19fbd9acbae7c7538199"}, + {file = "fonttools-4.55.2-cp310-cp310-win_amd64.whl", hash = "sha256:bbea0ab841113ac8e8edde067e099b7288ffc6ac2dded538b131c2c0595d5f77"}, + {file = "fonttools-4.55.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d34525e8141286fa976e14806639d32294bfb38d28bbdb5f6be9f46a1cd695a6"}, + {file = "fonttools-4.55.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ecd1c2b1c2ec46bb73685bc5473c72e16ed0930ef79bc2919ccadc43a99fb16"}, + {file = "fonttools-4.55.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9008438ad59e5a8e403a62fbefef2b2ff377eb3857d90a3f2a5f4d674ff441b2"}, + {file = "fonttools-4.55.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:131591ac8d7a47043aaf29581aba755ae151d46e49d2bf49608601efd71e8b4d"}, + {file = "fonttools-4.55.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4c83381c3e3e3d9caa25527c4300543578341f21aae89e4fbbb4debdda8d82a2"}, + {file = "fonttools-4.55.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:42aca564b575252fd9954ed0d91d97a24de24289a16ce8ff74ed0bdf5ecebf11"}, + {file = "fonttools-4.55.2-cp311-cp311-win32.whl", hash = "sha256:c6457f650ebe15baa17fc06e256227f0a47f46f80f27ec5a0b00160de8dc2c13"}, + {file = "fonttools-4.55.2-cp311-cp311-win_amd64.whl", hash = "sha256:5cfa67414d7414442a5635ff634384101c54f53bb7b0e04aa6a61b013fcce194"}, + {file = "fonttools-4.55.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:18f082445b8fe5e91c53e6184f4c1c73f3f965c8bcc614c6cd6effd573ce6c1a"}, + {file = "fonttools-4.55.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c0f91adbbd706e8acd1db73e3e510118e62d0ffb651864567dccc5b2339f90"}, + {file = "fonttools-4.55.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d8ccce035320d63dba0c35f52499322f5531dbe85bba1514c7cea26297e4c54"}, + {file = "fonttools-4.55.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96e126df9615df214ec7f04bebcf60076297fbc10b75c777ce58b702d7708ffb"}, + {file = "fonttools-4.55.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:508ebb42956a7a931c4092dfa2d9b4ffd4f94cea09b8211199090d2bd082506b"}, + {file = "fonttools-4.55.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c1b9de46ef7b683d50400abf9f1578eaceee271ff51c36bf4b7366f2be29f498"}, + {file = "fonttools-4.55.2-cp312-cp312-win32.whl", hash = "sha256:2df61d9fc15199cc86dad29f64dd686874a3a52dda0c2d8597d21f509f95c332"}, + {file = "fonttools-4.55.2-cp312-cp312-win_amd64.whl", hash = "sha256:d337ec087da8216a828574aa0525d869df0a2ac217a2efc1890974ddd1fbc5b9"}, + {file = "fonttools-4.55.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:10aff204e2edee1d312fa595c06f201adf8d528a3b659cfb34cd47eceaaa6a26"}, + {file = "fonttools-4.55.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:09fe922a3eff181fd07dd724cdb441fb6b9fc355fd1c0f1aa79aca60faf1fbdd"}, + {file = "fonttools-4.55.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:487e1e8b524143a799bda0169c48b44a23a6027c1bb1957d5a172a7d3a1dd704"}, + {file = "fonttools-4.55.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b1726872e09268bbedb14dc02e58b7ea31ecdd1204c6073eda4911746b44797"}, + {file = "fonttools-4.55.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6fc88cfb58b0cd7b48718c3e61dd0d0a3ee8e2c86b973342967ce09fbf1db6d4"}, + {file = "fonttools-4.55.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e857fe1859901ad8c5cab32e0eebc920adb09f413d2d73b74b677cf47b28590c"}, + {file = "fonttools-4.55.2-cp313-cp313-win32.whl", hash = "sha256:81ccd2b3a420b8050c7d9db3be0555d71662973b3ef2a1d921a2880b58957db8"}, + {file = "fonttools-4.55.2-cp313-cp313-win_amd64.whl", hash = "sha256:d559eb1744c7dcfa90ae60cb1a4b3595e898e48f4198738c321468c01180cd83"}, + {file = "fonttools-4.55.2-py3-none-any.whl", hash = "sha256:8e2d89fbe9b08d96e22c7a81ec04a4e8d8439c31223e2dc6f2f9fc8ff14bdf9f"}, + {file = "fonttools-4.55.2.tar.gz", hash = "sha256:45947e7b3f9673f91df125d375eb57b9a23f2a603f438a1aebf3171bffa7a205"}, ] [[package]] @@ -452,7 +452,7 @@ name = "kiwisolver" version = "1.4.7" requires_python = ">=3.8" summary = "A fast implementation of the Cassowary constraint solver" -groups = ["all"] +groups = ["all", "plotting"] files = [ {file = "kiwisolver-1.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8a9c83f75223d5e48b0bc9cb1bf2776cf01563e00ade8775ffe13b0b6e1af3a6"}, {file = "kiwisolver-1.4.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:58370b1ffbd35407444d57057b57da5d6549d2d854fa30249771775c63b5fe17"}, @@ -589,10 +589,10 @@ files = [ [[package]] name = "matplotlib" -version = "3.9.2" +version = "3.9.3" requires_python = ">=3.9" summary = "Python plotting package" -groups = ["all"] +groups = ["all", "plotting"] dependencies = [ "contourpy>=1.0.1", "cycler>=0.10", @@ -606,36 +606,37 @@ dependencies = [ "python-dateutil>=2.7", ] files = [ - {file = "matplotlib-3.9.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:9d78bbc0cbc891ad55b4f39a48c22182e9bdaea7fc0e5dbd364f49f729ca1bbb"}, - {file = "matplotlib-3.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c375cc72229614632c87355366bdf2570c2dac01ac66b8ad048d2dabadf2d0d4"}, - {file = "matplotlib-3.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d94ff717eb2bd0b58fe66380bd8b14ac35f48a98e7c6765117fe67fb7684e64"}, - {file = "matplotlib-3.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab68d50c06938ef28681073327795c5db99bb4666214d2d5f880ed11aeaded66"}, - {file = "matplotlib-3.9.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:65aacf95b62272d568044531e41de26285d54aec8cb859031f511f84bd8b495a"}, - {file = "matplotlib-3.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:3fd595f34aa8a55b7fc8bf9ebea8aa665a84c82d275190a61118d33fbc82ccae"}, - {file = "matplotlib-3.9.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d8dd059447824eec055e829258ab092b56bb0579fc3164fa09c64f3acd478772"}, - {file = "matplotlib-3.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c797dac8bb9c7a3fd3382b16fe8f215b4cf0f22adccea36f1545a6d7be310b41"}, - {file = "matplotlib-3.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d719465db13267bcef19ea8954a971db03b9f48b4647e3860e4bc8e6ed86610f"}, - {file = "matplotlib-3.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8912ef7c2362f7193b5819d17dae8629b34a95c58603d781329712ada83f9447"}, - {file = "matplotlib-3.9.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7741f26a58a240f43bee74965c4882b6c93df3e7eb3de160126d8c8f53a6ae6e"}, - {file = "matplotlib-3.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:ae82a14dab96fbfad7965403c643cafe6515e386de723e498cf3eeb1e0b70cc7"}, - {file = "matplotlib-3.9.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ac43031375a65c3196bee99f6001e7fa5bdfb00ddf43379d3c0609bdca042df9"}, - {file = "matplotlib-3.9.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:be0fc24a5e4531ae4d8e858a1a548c1fe33b176bb13eff7f9d0d38ce5112a27d"}, - {file = "matplotlib-3.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf81de2926c2db243c9b2cbc3917619a0fc85796c6ba4e58f541df814bbf83c7"}, - {file = "matplotlib-3.9.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6ee45bc4245533111ced13f1f2cace1e7f89d1c793390392a80c139d6cf0e6c"}, - {file = "matplotlib-3.9.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:306c8dfc73239f0e72ac50e5a9cf19cc4e8e331dd0c54f5e69ca8758550f1e1e"}, - {file = "matplotlib-3.9.2-cp312-cp312-win_amd64.whl", hash = "sha256:5413401594cfaff0052f9d8b1aafc6d305b4bd7c4331dccd18f561ff7e1d3bd3"}, - {file = "matplotlib-3.9.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:18128cc08f0d3cfff10b76baa2f296fc28c4607368a8402de61bb3f2eb33c7d9"}, - {file = "matplotlib-3.9.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4876d7d40219e8ae8bb70f9263bcbe5714415acfdf781086601211335e24f8aa"}, - {file = "matplotlib-3.9.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d9f07a80deab4bb0b82858a9e9ad53d1382fd122be8cde11080f4e7dfedb38b"}, - {file = "matplotlib-3.9.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7c0410f181a531ec4e93bbc27692f2c71a15c2da16766f5ba9761e7ae518413"}, - {file = "matplotlib-3.9.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:909645cce2dc28b735674ce0931a4ac94e12f5b13f6bb0b5a5e65e7cea2c192b"}, - {file = "matplotlib-3.9.2-cp313-cp313-win_amd64.whl", hash = "sha256:f32c7410c7f246838a77d6d1eff0c0f87f3cb0e7c4247aebea71a6d5a68cab49"}, - {file = "matplotlib-3.9.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:37e51dd1c2db16ede9cfd7b5cabdfc818b2c6397c83f8b10e0e797501c963a03"}, - {file = "matplotlib-3.9.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b82c5045cebcecd8496a4d694d43f9cc84aeeb49fe2133e036b207abe73f4d30"}, - {file = "matplotlib-3.9.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f053c40f94bc51bc03832a41b4f153d83f2062d88c72b5e79997072594e97e51"}, - {file = "matplotlib-3.9.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbe196377a8248972f5cede786d4c5508ed5f5ca4a1e09b44bda889958b33f8c"}, - {file = "matplotlib-3.9.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5816b1e1fe8c192cbc013f8f3e3368ac56fbecf02fb41b8f8559303f24c5015e"}, - {file = "matplotlib-3.9.2.tar.gz", hash = "sha256:96ab43906269ca64a6366934106fa01534454a69e471b7bf3d79083981aaab92"}, + {file = "matplotlib-3.9.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:41b016e3be4e740b66c79a031a0a6e145728dbc248142e751e8dab4f3188ca1d"}, + {file = "matplotlib-3.9.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e0143975fc2a6d7136c97e19c637321288371e8f09cff2564ecd73e865ea0b9"}, + {file = "matplotlib-3.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f459c8ee2c086455744723628264e43c884be0c7d7b45d84b8cd981310b4815"}, + {file = "matplotlib-3.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:687df7ceff57b8f070d02b4db66f75566370e7ae182a0782b6d3d21b0d6917dc"}, + {file = "matplotlib-3.9.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:edd14cf733fdc4f6e6fe3f705af97676a7e52859bf0044aa2c84e55be739241c"}, + {file = "matplotlib-3.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:1c40c244221a1adbb1256692b1133c6fb89418df27bf759a31a333e7912a4010"}, + {file = "matplotlib-3.9.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:cf2a60daf6cecff6828bc608df00dbc794380e7234d2411c0ec612811f01969d"}, + {file = "matplotlib-3.9.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:213d6dc25ce686516208d8a3e91120c6a4fdae4a3e06b8505ced5b716b50cc04"}, + {file = "matplotlib-3.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c52f48eb75fcc119a4fdb68ba83eb5f71656999420375df7c94cc68e0e14686e"}, + {file = "matplotlib-3.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3c93796b44fa111049b88a24105e947f03c01966b5c0cc782e2ee3887b790a3"}, + {file = "matplotlib-3.9.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cd1077b9a09b16d8c3c7075a8add5ffbfe6a69156a57e290c800ed4d435bef1d"}, + {file = "matplotlib-3.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:c96eeeb8c68b662c7747f91a385688d4b449687d29b691eff7068a4602fe6dc4"}, + {file = "matplotlib-3.9.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0a361bd5583bf0bcc08841df3c10269617ee2a36b99ac39d455a767da908bbbc"}, + {file = "matplotlib-3.9.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e14485bb1b83eeb3d55b6878f9560240981e7bbc7a8d4e1e8c38b9bd6ec8d2de"}, + {file = "matplotlib-3.9.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a8d279f78844aad213c4935c18f8292a9432d51af2d88bca99072c903948045"}, + {file = "matplotlib-3.9.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6c12514329ac0d03128cf1dcceb335f4fbf7c11da98bca68dca8dcb983153a9"}, + {file = "matplotlib-3.9.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6e9de2b390d253a508dd497e9b5579f3a851f208763ed67fdca5dc0c3ea6849c"}, + {file = "matplotlib-3.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:d796272408f8567ff7eaa00eb2856b3a00524490e47ad505b0b4ca6bb8a7411f"}, + {file = "matplotlib-3.9.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:203d18df84f5288973b2d56de63d4678cc748250026ca9e1ad8f8a0fd8a75d83"}, + {file = "matplotlib-3.9.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b651b0d3642991259109dc0351fc33ad44c624801367bb8307be9bfc35e427ad"}, + {file = "matplotlib-3.9.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:66d7b171fecf96940ce069923a08ba3df33ef542de82c2ff4fe8caa8346fa95a"}, + {file = "matplotlib-3.9.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6be0ba61f6ff2e6b68e4270fb63b6813c9e7dec3d15fc3a93f47480444fd72f0"}, + {file = "matplotlib-3.9.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d6b2e8856dec3a6db1ae51aec85c82223e834b228c1d3228aede87eee2b34f9"}, + {file = "matplotlib-3.9.3-cp313-cp313-win_amd64.whl", hash = "sha256:90a85a004fefed9e583597478420bf904bb1a065b0b0ee5b9d8d31b04b0f3f70"}, + {file = "matplotlib-3.9.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3119b2f16de7f7b9212ba76d8fe6a0e9f90b27a1e04683cd89833a991682f639"}, + {file = "matplotlib-3.9.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:87ad73763d93add1b6c1f9fcd33af662fd62ed70e620c52fcb79f3ac427cf3a6"}, + {file = "matplotlib-3.9.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:026bdf3137ab6022c866efa4813b6bbeddc2ed4c9e7e02f0e323a7bca380dfa0"}, + {file = "matplotlib-3.9.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:760a5e89ebbb172989e8273024a1024b0f084510b9105261b3b00c15e9c9f006"}, + {file = "matplotlib-3.9.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a42b9dc42de2cfe357efa27d9c50c7833fc5ab9b2eb7252ccd5d5f836a84e1e4"}, + {file = "matplotlib-3.9.3-cp313-cp313t-win_amd64.whl", hash = "sha256:e0fcb7da73fbf67b5f4bdaa57d85bb585a4e913d4a10f3e15b32baea56a67f0a"}, + {file = "matplotlib-3.9.3.tar.gz", hash = "sha256:cd5dbbc8e25cad5f706845c4d100e2c8b34691b412b93717ce38d8ae803bcfa5"}, ] [[package]] @@ -730,7 +731,7 @@ name = "numpy" version = "2.1.3" requires_python = ">=3.10" summary = "Fundamental package for array computing in Python" -groups = ["all"] +groups = ["all", "opus", "plotting"] files = [ {file = "numpy-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c894b4305373b9c5576d7a12b473702afdf48ce5369c074ba304cc5ad8730dff"}, {file = "numpy-2.1.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b47fbb433d3260adcd51eb54f92a2ffbc90a4595f8970ee00e064c644ac788f5"}, @@ -794,7 +795,7 @@ name = "packaging" version = "24.2" requires_python = ">=3.8" summary = "Core utilities for Python packages" -groups = ["all", "dev"] +groups = ["all", "dev", "plotting"] files = [ {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, @@ -816,7 +817,7 @@ name = "pillow" version = "11.0.0" requires_python = ">=3.9" summary = "Python Imaging Library (Fork)" -groups = ["all"] +groups = ["all", "plotting"] files = [ {file = "pillow-11.0.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6619654954dc4936fcff82db8eb6401d3159ec6be81e33c6000dfd76ae189947"}, {file = "pillow-11.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b3c5ac4bed7519088103d9450a1107f76308ecf91d6dabc8a33a2fcfb18d0fba"}, @@ -904,17 +905,17 @@ files = [ [[package]] name = "polars" -version = "1.13.1" +version = "1.16.0" requires_python = ">=3.9" summary = "Blazingly fast DataFrame library" -groups = ["all"] +groups = ["all", "em27", "polars"] files = [ - {file = "polars-1.13.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:9d5c74229fdc180fdbe99e4dc121a2ce0de6f0fcea2769a208f033112d5729dd"}, - {file = "polars-1.13.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:faa436c721179fca978a470ade1072acc5e510396a88ce7e3aa4fcc75186739f"}, - {file = "polars-1.13.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:405826a78c20721d0f47ee58bafdbd5311551c306cc52ff2e8dc0e2f5fc53d07"}, - {file = "polars-1.13.1-cp39-abi3-manylinux_2_24_aarch64.whl", hash = "sha256:792d9de49de6ebcfb137885e1643e09b35bcad1ae3bc86971f0d82a06372e1a4"}, - {file = "polars-1.13.1-cp39-abi3-win_amd64.whl", hash = "sha256:060148c687920c7af2dc16a9de0aa6de293233f1a2634db503c497504fdb19ad"}, - {file = "polars-1.13.1.tar.gz", hash = "sha256:a8a7bb70aca0657939552a4505eccabb07c9d59d330d5a66409fe67295082860"}, + {file = "polars-1.16.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:072f5ff3b5fe05797c59890de0e464b34ede75a9735e7d7221622fa3a0616d8e"}, + {file = "polars-1.16.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ebaf7a1ea114b042fa9f1cd17d49436279eb30545dd74361a2f5e3febeb867cd"}, + {file = "polars-1.16.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e626d21dcd2566e1442dac414fe177bc70ebfc2f16620d59d778b1b774361018"}, + {file = "polars-1.16.0-cp39-abi3-manylinux_2_24_aarch64.whl", hash = "sha256:53debcce55f68731ee2c7d6c787afdee26860ed6576f1ffa0cb9111b57f82857"}, + {file = "polars-1.16.0-cp39-abi3-win_amd64.whl", hash = "sha256:17efcb550c42d51034ff79702612b9184d8eac0d500de1dd7fb98490459276d3"}, + {file = "polars-1.16.0.tar.gz", hash = "sha256:dd99808b833872babe02434a809fd45c1cffe66a3d57123cdc5e447c7753d328"}, ] [[package]] @@ -936,24 +937,23 @@ files = [ [[package]] name = "pydantic" -version = "2.9.2" +version = "2.10.3" requires_python = ">=3.8" summary = "Data validation using Python type hints" groups = ["default"] dependencies = [ "annotated-types>=0.6.0", - "pydantic-core==2.23.4", - "typing-extensions>=4.12.2; python_version >= \"3.13\"", - "typing-extensions>=4.6.1; python_version < \"3.13\"", + "pydantic-core==2.27.1", + "typing-extensions>=4.12.2", ] files = [ - {file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"}, - {file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"}, + {file = "pydantic-2.10.3-py3-none-any.whl", hash = "sha256:be04d85bbc7b65651c5f8e6b9976ed9c6f41782a55524cef079a34a0bb82144d"}, + {file = "pydantic-2.10.3.tar.gz", hash = "sha256:cb5ac360ce894ceacd69c403187900a02c4b20b693a9dd1d643e1effab9eadf9"}, ] [[package]] name = "pydantic-core" -version = "2.23.4" +version = "2.27.1" requires_python = ">=3.8" summary = "Core functionality for Pydantic validation and serialization" groups = ["default"] @@ -961,63 +961,71 @@ dependencies = [ "typing-extensions!=4.7.0,>=4.6.0", ] files = [ - {file = "pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b"}, - {file = "pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"}, - {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb"}, - {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916"}, - {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07"}, - {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232"}, - {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2"}, - {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f"}, - {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3"}, - {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071"}, - {file = "pydantic_core-2.23.4-cp310-none-win32.whl", hash = "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119"}, - {file = "pydantic_core-2.23.4-cp310-none-win_amd64.whl", hash = "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f"}, - {file = "pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8"}, - {file = "pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d"}, - {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e"}, - {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607"}, - {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd"}, - {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea"}, - {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e"}, - {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b"}, - {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0"}, - {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64"}, - {file = "pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f"}, - {file = "pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3"}, - {file = "pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231"}, - {file = "pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee"}, - {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87"}, - {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8"}, - {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327"}, - {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2"}, - {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36"}, - {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126"}, - {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e"}, - {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24"}, - {file = "pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84"}, - {file = "pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9"}, - {file = "pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc"}, - {file = "pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327"}, - {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6"}, - {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f"}, - {file = "pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769"}, - {file = "pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753"}, - {file = "pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863"}, + {file = "pydantic_core-2.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a"}, + {file = "pydantic_core-2.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:121ceb0e822f79163dd4699e4c54f5ad38b157084d97b34de8b232bcaad70278"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4603137322c18eaf2e06a4495f426aa8d8388940f3c457e7548145011bb68e05"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a33cd6ad9017bbeaa9ed78a2e0752c5e250eafb9534f308e7a5f7849b0b1bfb4"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15cc53a3179ba0fcefe1e3ae50beb2784dede4003ad2dfd24f81bba4b23a454f"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45d9c5eb9273aa50999ad6adc6be5e0ecea7e09dbd0d31bd0c65a55a2592ca08"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8bf7b66ce12a2ac52d16f776b31d16d91033150266eb796967a7e4621707e4f6"}, + {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:655d7dd86f26cb15ce8a431036f66ce0318648f8853d709b4167786ec2fa4807"}, + {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:5556470f1a2157031e676f776c2bc20acd34c1990ca5f7e56f1ebf938b9ab57c"}, + {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f69ed81ab24d5a3bd93861c8c4436f54afdf8e8cc421562b0c7504cf3be58206"}, + {file = "pydantic_core-2.27.1-cp310-none-win32.whl", hash = "sha256:f5a823165e6d04ccea61a9f0576f345f8ce40ed533013580e087bd4d7442b52c"}, + {file = "pydantic_core-2.27.1-cp310-none-win_amd64.whl", hash = "sha256:57866a76e0b3823e0b56692d1a0bf722bffb324839bb5b7226a7dbd6c9a40b17"}, + {file = "pydantic_core-2.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8"}, + {file = "pydantic_core-2.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e"}, + {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919"}, + {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c"}, + {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc"}, + {file = "pydantic_core-2.27.1-cp311-none-win32.whl", hash = "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9"}, + {file = "pydantic_core-2.27.1-cp311-none-win_amd64.whl", hash = "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5"}, + {file = "pydantic_core-2.27.1-cp311-none-win_arm64.whl", hash = "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89"}, + {file = "pydantic_core-2.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f"}, + {file = "pydantic_core-2.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089"}, + {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381"}, + {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb"}, + {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae"}, + {file = "pydantic_core-2.27.1-cp312-none-win32.whl", hash = "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c"}, + {file = "pydantic_core-2.27.1-cp312-none-win_amd64.whl", hash = "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16"}, + {file = "pydantic_core-2.27.1-cp312-none-win_arm64.whl", hash = "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e"}, + {file = "pydantic_core-2.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073"}, + {file = "pydantic_core-2.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a"}, + {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc"}, + {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960"}, + {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23"}, + {file = "pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05"}, + {file = "pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337"}, + {file = "pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3fa80ac2bd5856580e242dbc202db873c60a01b20309c8319b5c5986fbe53ce6"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d950caa237bb1954f1b8c9227b5065ba6875ac9771bb8ec790d956a699b78676"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e4216e64d203e39c62df627aa882f02a2438d18a5f21d7f721621f7a5d3611d"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02a3d637bd387c41d46b002f0e49c52642281edacd2740e5a42f7017feea3f2c"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:161c27ccce13b6b0c8689418da3885d3220ed2eae2ea5e9b2f7f3d48f1d52c27"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:19910754e4cc9c63bc1c7f6d73aa1cfee82f42007e407c0f413695c2f7ed777f"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e173486019cc283dc9778315fa29a363579372fe67045e971e89b6365cc035ed"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:af52d26579b308921b73b956153066481f064875140ccd1dfd4e77db89dbb12f"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:981fb88516bd1ae8b0cbbd2034678a39dedc98752f264ac9bc5839d3923fa04c"}, + {file = "pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235"}, ] [[package]] @@ -1067,7 +1075,7 @@ name = "pyparsing" version = "3.2.0" requires_python = ">=3.9" summary = "pyparsing module - Classes and methods to define and execute parsing grammars" -groups = ["all"] +groups = ["all", "plotting"] files = [ {file = "pyparsing-3.2.0-py3-none-any.whl", hash = "sha256:93d9577b88da0bbea8cc8334ee8b918ed014968fd2ec383e868fb8afb1ccef84"}, {file = "pyparsing-3.2.0.tar.gz", hash = "sha256:cbf74e27246d595d9a74b186b810f6fbb86726dbf3b9532efb343f6d7294fe9c"}, @@ -1075,7 +1083,7 @@ files = [ [[package]] name = "pytest" -version = "8.3.3" +version = "8.3.4" requires_python = ">=3.8" summary = "pytest: simple powerful testing with Python" groups = ["dev"] @@ -1088,8 +1096,8 @@ dependencies = [ "tomli>=1; python_version < \"3.11\"", ] files = [ - {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, - {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, + {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, + {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, ] [[package]] @@ -1097,7 +1105,7 @@ name = "python-dateutil" version = "2.9.0.post0" requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" summary = "Extensions to the standard Python datetime module" -groups = ["all"] +groups = ["all", "plotting"] dependencies = [ "six>=1.5", ] @@ -1179,15 +1187,42 @@ files = [ {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, ] +[[package]] +name = "ruff" +version = "0.8.2" +requires_python = ">=3.7" +summary = "An extremely fast Python linter and code formatter, written in Rust." +groups = ["dev"] +files = [ + {file = "ruff-0.8.2-py3-none-linux_armv6l.whl", hash = "sha256:c49ab4da37e7c457105aadfd2725e24305ff9bc908487a9bf8d548c6dad8bb3d"}, + {file = "ruff-0.8.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ec016beb69ac16be416c435828be702ee694c0d722505f9c1f35e1b9c0cc1bf5"}, + {file = "ruff-0.8.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f05cdf8d050b30e2ba55c9b09330b51f9f97d36d4673213679b965d25a785f3c"}, + {file = "ruff-0.8.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60f578c11feb1d3d257b2fb043ddb47501ab4816e7e221fbb0077f0d5d4e7b6f"}, + {file = "ruff-0.8.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cbd5cf9b0ae8f30eebc7b360171bd50f59ab29d39f06a670b3e4501a36ba5897"}, + {file = "ruff-0.8.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b402ddee3d777683de60ff76da801fa7e5e8a71038f57ee53e903afbcefdaa58"}, + {file = "ruff-0.8.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:705832cd7d85605cb7858d8a13d75993c8f3ef1397b0831289109e953d833d29"}, + {file = "ruff-0.8.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:32096b41aaf7a5cc095fa45b4167b890e4c8d3fd217603f3634c92a541de7248"}, + {file = "ruff-0.8.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e769083da9439508833cfc7c23e351e1809e67f47c50248250ce1ac52c21fb93"}, + {file = "ruff-0.8.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fe716592ae8a376c2673fdfc1f5c0c193a6d0411f90a496863c99cd9e2ae25d"}, + {file = "ruff-0.8.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:81c148825277e737493242b44c5388a300584d73d5774defa9245aaef55448b0"}, + {file = "ruff-0.8.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d261d7850c8367704874847d95febc698a950bf061c9475d4a8b7689adc4f7fa"}, + {file = "ruff-0.8.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1ca4e3a87496dc07d2427b7dd7ffa88a1e597c28dad65ae6433ecb9f2e4f022f"}, + {file = "ruff-0.8.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:729850feed82ef2440aa27946ab39c18cb4a8889c1128a6d589ffa028ddcfc22"}, + {file = "ruff-0.8.2-py3-none-win32.whl", hash = "sha256:ac42caaa0411d6a7d9594363294416e0e48fc1279e1b0e948391695db2b3d5b1"}, + {file = "ruff-0.8.2-py3-none-win_amd64.whl", hash = "sha256:2aae99ec70abf43372612a838d97bfe77d45146254568d94926e8ed5bbb409ea"}, + {file = "ruff-0.8.2-py3-none-win_arm64.whl", hash = "sha256:fb88e2a506b70cfbc2de6fae6681c4f944f7dd5f2fe87233a7233d888bad73e8"}, + {file = "ruff-0.8.2.tar.gz", hash = "sha256:b84f4f414dda8ac7f75075c1fa0b905ac0ff25361f42e6d5da681a465e0f78e5"}, +] + [[package]] name = "six" -version = "1.16.0" -requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +version = "1.17.0" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" summary = "Python 2 and 3 compatibility utilities" -groups = ["all"] +groups = ["all", "plotting"] files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] [[package]] @@ -1213,13 +1248,43 @@ files = [ [[package]] name = "tomli" -version = "2.1.0" +version = "2.2.1" requires_python = ">=3.8" summary = "A lil' TOML parser" groups = ["dev"] files = [ - {file = "tomli-2.1.0-py3-none-any.whl", hash = "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391"}, - {file = "tomli-2.1.0.tar.gz", hash = "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] [[package]] @@ -1341,43 +1406,58 @@ files = [ [[package]] name = "wrapt" -version = "1.16.0" -requires_python = ">=3.6" +version = "1.17.0" +requires_python = ">=3.8" summary = "Module for decorators, wrappers and monkey patching." groups = ["dev"] files = [ - {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, - {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, - {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, - {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, - {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"}, - {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, - {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, - {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, - {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, - {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, - {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, - {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, - {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, - {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, - {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, - {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, - {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, - {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, - {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, - {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, - {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, - {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, - {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, - {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, - {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, - {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, - {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, - {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, - {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, - {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, - {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, - {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, + {file = "wrapt-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a0c23b8319848426f305f9cb0c98a6e32ee68a36264f45948ccf8e7d2b941f8"}, + {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1ca5f060e205f72bec57faae5bd817a1560fcfc4af03f414b08fa29106b7e2d"}, + {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e185ec6060e301a7e5f8461c86fb3640a7beb1a0f0208ffde7a65ec4074931df"}, + {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb90765dd91aed05b53cd7a87bd7f5c188fcd95960914bae0d32c5e7f899719d"}, + {file = "wrapt-1.17.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:879591c2b5ab0a7184258274c42a126b74a2c3d5a329df16d69f9cee07bba6ea"}, + {file = "wrapt-1.17.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fce6fee67c318fdfb7f285c29a82d84782ae2579c0e1b385b7f36c6e8074fffb"}, + {file = "wrapt-1.17.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0698d3a86f68abc894d537887b9bbf84d29bcfbc759e23f4644be27acf6da301"}, + {file = "wrapt-1.17.0-cp310-cp310-win32.whl", hash = "sha256:69d093792dc34a9c4c8a70e4973a3361c7a7578e9cd86961b2bbf38ca71e4e22"}, + {file = "wrapt-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:f28b29dc158ca5d6ac396c8e0a2ef45c4e97bb7e65522bfc04c989e6fe814575"}, + {file = "wrapt-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:74bf625b1b4caaa7bad51d9003f8b07a468a704e0644a700e936c357c17dd45a"}, + {file = "wrapt-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f2a28eb35cf99d5f5bd12f5dd44a0f41d206db226535b37b0c60e9da162c3ed"}, + {file = "wrapt-1.17.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:81b1289e99cf4bad07c23393ab447e5e96db0ab50974a280f7954b071d41b489"}, + {file = "wrapt-1.17.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f2939cd4a2a52ca32bc0b359015718472d7f6de870760342e7ba295be9ebaf9"}, + {file = "wrapt-1.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6a9653131bda68a1f029c52157fd81e11f07d485df55410401f745007bd6d339"}, + {file = "wrapt-1.17.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4e4b4385363de9052dac1a67bfb535c376f3d19c238b5f36bddc95efae15e12d"}, + {file = "wrapt-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bdf62d25234290db1837875d4dceb2151e4ea7f9fff2ed41c0fde23ed542eb5b"}, + {file = "wrapt-1.17.0-cp311-cp311-win32.whl", hash = "sha256:5d8fd17635b262448ab8f99230fe4dac991af1dabdbb92f7a70a6afac8a7e346"}, + {file = "wrapt-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:92a3d214d5e53cb1db8b015f30d544bc9d3f7179a05feb8f16df713cecc2620a"}, + {file = "wrapt-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:89fc28495896097622c3fc238915c79365dd0ede02f9a82ce436b13bd0ab7569"}, + {file = "wrapt-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:875d240fdbdbe9e11f9831901fb8719da0bd4e6131f83aa9f69b96d18fae7504"}, + {file = "wrapt-1.17.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ed16d95fd142e9c72b6c10b06514ad30e846a0d0917ab406186541fe68b451"}, + {file = "wrapt-1.17.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18b956061b8db634120b58f668592a772e87e2e78bc1f6a906cfcaa0cc7991c1"}, + {file = "wrapt-1.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:daba396199399ccabafbfc509037ac635a6bc18510ad1add8fd16d4739cdd106"}, + {file = "wrapt-1.17.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4d63f4d446e10ad19ed01188d6c1e1bb134cde8c18b0aa2acfd973d41fcc5ada"}, + {file = "wrapt-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8a5e7cc39a45fc430af1aefc4d77ee6bad72c5bcdb1322cfde852c15192b8bd4"}, + {file = "wrapt-1.17.0-cp312-cp312-win32.whl", hash = "sha256:0a0a1a1ec28b641f2a3a2c35cbe86c00051c04fffcfcc577ffcdd707df3f8635"}, + {file = "wrapt-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:3c34f6896a01b84bab196f7119770fd8466c8ae3dfa73c59c0bb281e7b588ce7"}, + {file = "wrapt-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:714c12485aa52efbc0fc0ade1e9ab3a70343db82627f90f2ecbc898fdf0bb181"}, + {file = "wrapt-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da427d311782324a376cacb47c1a4adc43f99fd9d996ffc1b3e8529c4074d393"}, + {file = "wrapt-1.17.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba1739fb38441a27a676f4de4123d3e858e494fac05868b7a281c0a383c098f4"}, + {file = "wrapt-1.17.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e711fc1acc7468463bc084d1b68561e40d1eaa135d8c509a65dd534403d83d7b"}, + {file = "wrapt-1.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:140ea00c87fafc42739bd74a94a5a9003f8e72c27c47cd4f61d8e05e6dec8721"}, + {file = "wrapt-1.17.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:73a96fd11d2b2e77d623a7f26e004cc31f131a365add1ce1ce9a19e55a1eef90"}, + {file = "wrapt-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0b48554952f0f387984da81ccfa73b62e52817a4386d070c75e4db7d43a28c4a"}, + {file = "wrapt-1.17.0-cp313-cp313-win32.whl", hash = "sha256:498fec8da10e3e62edd1e7368f4b24aa362ac0ad931e678332d1b209aec93045"}, + {file = "wrapt-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd136bb85f4568fffca995bd3c8d52080b1e5b225dbf1c2b17b66b4c5fa02838"}, + {file = "wrapt-1.17.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:17fcf043d0b4724858f25b8826c36e08f9fb2e475410bece0ec44a22d533da9b"}, + {file = "wrapt-1.17.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4a557d97f12813dc5e18dad9fa765ae44ddd56a672bb5de4825527c847d6379"}, + {file = "wrapt-1.17.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0229b247b0fc7dee0d36176cbb79dbaf2a9eb7ecc50ec3121f40ef443155fb1d"}, + {file = "wrapt-1.17.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8425cfce27b8b20c9b89d77fb50e368d8306a90bf2b6eef2cdf5cd5083adf83f"}, + {file = "wrapt-1.17.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9c900108df470060174108012de06d45f514aa4ec21a191e7ab42988ff42a86c"}, + {file = "wrapt-1.17.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4e547b447073fc0dbfcbff15154c1be8823d10dab4ad401bdb1575e3fdedff1b"}, + {file = "wrapt-1.17.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:914f66f3b6fc7b915d46c1cc424bc2441841083de01b90f9e81109c9759e43ab"}, + {file = "wrapt-1.17.0-cp313-cp313t-win32.whl", hash = "sha256:a4192b45dff127c7d69b3bdfb4d3e47b64179a0b9900b6351859f3001397dabf"}, + {file = "wrapt-1.17.0-cp313-cp313t-win_amd64.whl", hash = "sha256:4f643df3d4419ea3f856c5c3f40fec1d65ea2e89ec812c83f7767c8730f9827a"}, + {file = "wrapt-1.17.0-py3-none-any.whl", hash = "sha256:d2c63b93548eda58abf5188e505ffed0229bf675f7c3090f8e36ad55b8cbc371"}, + {file = "wrapt-1.17.0.tar.gz", hash = "sha256:16187aa2317c731170a88ef35e8937ae0f533c402872c1ee5e6d079fcf320801"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index cbaf002..e1c4b28 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,8 +1,11 @@ [project] name = "tum_esm_utils" -version = "2.4.0" +version = "2.5.0" description = "Python utilities by the Professorship of Environmental Sensing and Modeling at the Technical University of Munich" -authors = [{ name = "Moritz Makowski", email = "moritz.makowski@tum.de" }] +authors = [ + { name = "Moritz Makowski", email = "moritz.makowski@tum.de" }, + { name = "Friedrich Klappenbach", email = "ge79wul@mytum.de" }, +] dependencies = [ "filelock>=3.13.4", "requests>=2.31.0", @@ -43,17 +46,18 @@ documentation = "https://tum-esm-utils.netlify.app" dev = [ "pytest>=8.1.1", "pydocstyle>=6.3.0", - "yapf>=0.40.2", "mypy>=1.13.0", "pydoc-markdown>=4.8.2", "types-psutil>=5.9.5.20240316", "types-requests>=2.31.0.20240406", "types-pytz>=2024.1.0.20240203", + "ruff>=0.8.1", ] plotting = ["matplotlib>=3.9.2", "numpy>=2.0.2"] -polars = ["polars>=1.4.1"] +em27 = ["polars>=1.4.1"] +polars = ["polars>=1.4.1"] # alias for em27 (will be removed with 3.0) all = ["matplotlib>=3.9.2", "numpy>=2.0.2", "polars>=1.4.1"] - +opus = ["numpy>=2.1.3"] [build-system] requires = ["pdm-backend"] @@ -67,12 +71,8 @@ strict = true implicit_reexport = true plugins = ["pydantic.mypy"] -[tool.yapf] -based_on_style = "facebook" -coalesce_brackets = true -split_before_dict_set_generator = true -each_dict_entry_on_separate_line = false -spaces_around_subscript_colon = true +[tool.ruff] +line-length = 100 [tool.pdm] distribution = true diff --git a/tests/data/ma20240514s0e00a.0975 b/tests/data/ma20240514s0e00a.0975 new file mode 100755 index 0000000..dd85704 Binary files /dev/null and b/tests/data/ma20240514s0e00a.0975 differ diff --git a/tests/data/md20220409s0e00a.0198.json b/tests/data/md20220409s0e00a.0198.json new file mode 100644 index 0000000..c0ff4ca --- /dev/null +++ b/tests/data/md20220409s0e00a.0198.json @@ -0,0 +1,238 @@ +{ + "header": { + "version": 920622, + "dir_pointer": 24, + "max_dir_size": 40, + "dir_size": 11 + }, + "channel_parameters": [ + { + "spectrum": { + "DPF": 1, + "NPT": 228512, + "NSN": -1, + "TPX": -1, + "FXV": 0.0, + "LXV": 228511.0, + "CSF": 0.25, + "MXY": 5.399981455411762e-5, + "MNY": 5.0662565627135336e-5, + "DXU": "PNT", + "DAT": "09/04/2022", + "TIM": "11:39:33.575 (GMT+0)", + "END": null + }, + "instrument": { + "HFL": 15797.798, + "LFL": 0.0, + "LWN": 15797.798, + "ABP": 59912, + "SSP": 1, + "ASG": 4, + "ARG": 8, + "ASS": 2, + "GFW": 1, + "GBW": 1, + "BFW": 0, + "BBW": 0, + "PKA": 1, + "PKL": 57128, + "PRA": 1, + "PRL": 57128, + "P2A": 0, + "P2L": 79401, + "P2R": 0, + "P2K": 86876, + "DAQ": 0, + "AG2": 4, + "SSM": 2, + "RSN": 1144839, + "CRR": 0, + "SRT": 1649504373.575, + "DUR": 11.739997863769531, + "TSC": 39.5, + "MVD": 0.38898974657058716, + "AN1": 0.22261009365320206, + "AN2": 0.0, + "VSN": "2.485 Apr 4 2017", + "SRN": "MID0000", + "CAM": 0, + "INS": "EM27/SUN", + "FOC": 127.0, + "RDY": "1", + "END": null + }, + "acquisition": { + "ADT": 0, + "AQM": "DD", + "CFE": "0", + "COR": "0", + "DEL": 0, + "DLY": 0, + "HFW": 15797.0, + "LFW": 0.0, + "NSS": 2, + "PLF": "TR", + "RES": 0.5, + "SOT": "0", + "TCL": "H2Ocomp ([:ScSm], [:ScRf], {H2O=7});", + "TDL": 2, + "SGN": "4", + "SG2": "4", + "END": null + }, + "optics": { + "CHN": "External Input", + "DTC": "RT-InGaAs_dual_ DC [Internal]", + "HPF": "0", + "LPF": "10.", + "PGN": "0", + "RDX": "0", + "VEL": "10.", + "SON": "Off", + "END": null + }, + "sample": { + "BLD": "", + "CNM": "Default", + "CPY": "", + "DPM": "", + "EXP": "EM27SUNdual_munichcampaign.xpm", + "LCT": "", + "SFM": "", + "SNM": "s0e00a", + "XPP": "C:\\Users\\Public\\Documents\\Bruker\\OPUS_7.8.44\\XPM", + "IST": "OK", + "CPG": 1252, + "END": null + }, + "fourier_transform": { + "APF": "NBM", + "HFQ": 15797.0, + "LFQ": 100.0, + "NLI": 0, + "PHR": 4.0, + "PHZ": "ML", + "SPZ": "NO", + "ZFF": "8", + "END": null + } + }, + { + "spectrum": { + "DPF": 1, + "NPT": 228512, + "NSN": -1, + "TPX": -1, + "FXV": 0.0, + "LXV": 228511.0, + "CSF": 0.25, + "MXY": 9.815269550017547e-6, + "MNY": 5.05696971231373e-6, + "DXU": "PNT", + "DAT": "09/04/2022", + "TIM": "11:39:33.575 (GMT+0)", + "END": null + }, + "instrument": { + "HFL": 15797.798, + "LFL": 0.0, + "LWN": 15797.798, + "ABP": 59912, + "SSP": 1, + "ASG": 4, + "ARG": 8, + "ASS": 2, + "GFW": 1, + "GBW": 1, + "BFW": 0, + "BBW": 0, + "PKA": 1, + "PKL": 57128, + "PRA": 1, + "PRL": 57128, + "P2A": 0, + "P2L": 79401, + "P2R": 0, + "P2K": 86876, + "DAQ": 0, + "AG2": 4, + "SSM": 2, + "RSN": 1144839, + "CRR": 0, + "SRT": 1649504373.575, + "DUR": 11.739997863769531, + "TSC": 39.5, + "MVD": 0.38898974657058716, + "AN1": 0.22261009365320206, + "AN2": 0.0, + "VSN": "2.485 Apr 4 2017", + "SRN": "MID0000", + "CAM": 0, + "INS": "EM27/SUN", + "FOC": 127.0, + "RDY": "1", + "END": null + }, + "acquisition": { + "ADT": 0, + "AQM": "DD", + "CFE": "0", + "COR": "0", + "DEL": 0, + "DLY": 0, + "HFW": 15797.0, + "LFW": 0.0, + "NSS": 2, + "PLF": "TR", + "RES": 0.5, + "SOT": "0", + "TCL": "H2Ocomp ([:ScSm], [:ScRf], {H2O=7});", + "TDL": 2, + "SGN": "4", + "SG2": "4", + "END": null + }, + "optics": { + "CHN": "External Input", + "DTC": "RT-InGaAs_dual_ DC [Internal]", + "HPF": "0", + "LPF": "10.", + "PGN": "0", + "RDX": "0", + "VEL": "10.", + "SON": "Off", + "END": null + }, + "sample": { + "BLD": "", + "CNM": "Default", + "CPY": "", + "DPM": "", + "EXP": "EM27SUNdual_munichcampaign.xpm", + "LCT": "", + "SFM": "", + "SNM": "s0e00a", + "XPP": "C:\\Users\\Public\\Documents\\Bruker\\OPUS_7.8.44\\XPM", + "IST": "OK", + "CPG": 1252, + "END": null + }, + "fourier_transform": { + "APF": "NBM", + "HFQ": 15797.0, + "LFQ": 100.0, + "NLI": 0, + "PHR": 4.0, + "PHZ": "ML", + "SPZ": "NO", + "ZFF": "8", + "END": null + } + } + ], + "measurement_times": [ + "2022-04-09T11:39:39.444999+00:00", + "2022-04-09T11:39:39.444999+00:00" + ] +} diff --git a/tests/test_em27.py b/tests/test_em27.py index ef809d8..2dbb2e6 100644 --- a/tests/test_em27.py +++ b/tests/test_em27.py @@ -2,9 +2,7 @@ import tempfile import tum_esm_utils -_PROJECT_DIR = tum_esm_utils.files.get_parent_dir_path( - __file__, current_depth=2 -) +_PROJECT_DIR = tum_esm_utils.files.get_parent_dir_path(__file__, current_depth=2) def test_detect_corrupt_ifgs() -> None: @@ -16,30 +14,27 @@ def test_detect_corrupt_ifgs() -> None: test_file_name = os.path.join(tmpdir, "test_ifg") with open(test_file_name, "w") as f: f.write("corrupt interferogram") - assert set(tum_esm_utils.em27.detect_corrupt_opus_files(tmpdir).keys() - ) == {"test_ifg"} + assert set(tum_esm_utils.em27.detect_corrupt_opus_files(tmpdir).keys()) == {"test_ifg"} detection_results = tum_esm_utils.em27.detect_corrupt_opus_files( tum_esm_utils.files.rel_to_abs_path("./data") ) - assert set(detection_results.keys()) == set([ - "comb_invparms_ma_SN061_210329-210329.csv", - "comb_invparms_mc_SN115_220602-220602.csv", - "md20220409s0e00a.0199", - "md20220409s0e00a.0200", - ]) + assert set(detection_results.keys()) == set( + [ + "comb_invparms_ma_SN061_210329-210329.csv", + "comb_invparms_mc_SN115_220602-220602.csv", + "md20220409s0e00a.0199", + "md20220409s0e00a.0200", + "md20220409s0e00a.0198.json", + ] + ) def test_load_proffast2_result() -> None: input_dir = tum_esm_utils.files.rel_to_abs_path("./data") - files = [ - f for f in os.listdir(input_dir) - if f.startswith("comb_inv") and f.endswith(".csv") - ] + files = [f for f in os.listdir(input_dir) if f.startswith("comb_inv") and f.endswith(".csv")] for f in files: - df = tum_esm_utils.em27.load_proffast2_result( - os.path.join(input_dir, f) - ) + df = tum_esm_utils.em27.load_proffast2_result(os.path.join(input_dir, f)) assert len(df) > 2 assert "UTC" in df.columns assert "XCO2" in df.columns diff --git a/tests/test_opus.py b/tests/test_opus.py new file mode 100644 index 0000000..250d693 --- /dev/null +++ b/tests/test_opus.py @@ -0,0 +1,36 @@ +import numpy as np +import tum_esm_utils + +IFG1 = tum_esm_utils.files.rel_to_abs_path("./data/md20220409s0e00a.0198") +IFG2 = tum_esm_utils.files.rel_to_abs_path("./data/md20220409s0e00a.0199") +IFG3 = tum_esm_utils.files.rel_to_abs_path("./data/md20220409s0e00a.0200") +IFG4 = tum_esm_utils.files.rel_to_abs_path("./data/ma20240514s0e00a.0975") + + +def test_opus_file_reading() -> None: + for mode in ["skip", "validate", "read"]: + of = tum_esm_utils.opus.OpusFile.read(IFG1, interferogram_mode=mode) # type: ignore + assert (of.interferogram is not None) == (mode == "read") + of.model_dump() + + # Expect invalid OPUS files to raise a RuntimeError + try: + tum_esm_utils.opus.OpusFile.read(IFG2, interferogram_mode=mode) # type: ignore + raise Exception("Expected a RuntimeError") + except RuntimeError: + pass + try: + tum_esm_utils.opus.OpusFile.read(IFG3, interferogram_mode=mode) # type: ignore + raise Exception("Expected a RuntimeError") + except RuntimeError: + pass + + # Compute peak position of both channels + + ifg = tum_esm_utils.opus.OpusFile.read(IFG4, interferogram_mode="read").interferogram + assert ifg is not None + fwd = ifg[0][: ifg.shape[1] // 2] + assert len(fwd) == 114256 + computed_peak = np.argmax(fwd) + ifg_center = fwd.shape[0] // 2 + assert abs(computed_peak - ifg_center) < 10 diff --git a/tests/test_with_filelock.py b/tests/test_with_filelock.py index 4893046..a4d3edf 100644 --- a/tests/test_with_filelock.py +++ b/tests/test_with_filelock.py @@ -3,9 +3,12 @@ import os import time import queue -import threading -import multiprocessing import tum_esm_utils +import multiprocessing + +multiprocessing.set_start_method("spawn", force=True) + +import threading TIMEOUT_UNIT = 0.5 res_queue_th: queue.Queue[int] = queue.Queue() @@ -26,9 +29,7 @@ def count_queue_items(q: queue.Queue[int]) -> int: return c -@tum_esm_utils.decorators.with_filelock( - lockfile_path=lockfile_path, timeout=TIMEOUT_UNIT * 2 -) +@tum_esm_utils.decorators.with_filelock(lockfile_path=lockfile_path, timeout=TIMEOUT_UNIT * 2) def f(delay: int = 0, q: Optional[queue.Queue[int]] = None) -> int: """this funtion will sleep for the amount passed as `delay`, put 1 into the passed queue (optional) and return 1. It will raise a @@ -73,24 +74,16 @@ def test_filelock_with_multiprocessing() -> None: """takes quite long because I had to increase `TIMEOUT_UNIT` to `3` for it to work on GitHub's CI small runners""" - t1 = multiprocessing.Process( - target=f, kwargs={"delay": 1, "q": res_queue_mp} - ) - t2 = multiprocessing.Process( - target=f, kwargs={"delay": 1, "q": res_queue_mp} - ) + t1 = multiprocessing.Process(target=f, kwargs={"delay": 1, "q": res_queue_mp}) + t2 = multiprocessing.Process(target=f, kwargs={"delay": 1, "q": res_queue_mp}) t1.start() t2.start() t1.join() t2.join() assert count_queue_items(res_queue_mp) == 2 - t3 = multiprocessing.Process( - target=f, kwargs={"delay": 3, "q": res_queue_mp} - ) - t4 = multiprocessing.Process( - target=f, kwargs={"delay": 3, "q": res_queue_mp} - ) + t3 = multiprocessing.Process(target=f, kwargs={"delay": 3, "q": res_queue_mp}) + t4 = multiprocessing.Process(target=f, kwargs={"delay": 3, "q": res_queue_mp}) t3.start() t4.start() t3.join() diff --git a/tum_esm_utils/__init__.py b/tum_esm_utils/__init__.py index 9e46954..792a802 100644 --- a/tum_esm_utils/__init__.py +++ b/tum_esm_utils/__init__.py @@ -22,7 +22,7 @@ # ignore import errors from the following submodules # because they requires extras to be installed -# requires extra "polars" +# requires extra "em27" try: from . import em27 except ImportError: @@ -33,3 +33,9 @@ from . import plotting except ImportError: pass + +# requires extra "opus" +try: + from . import opus +except ImportError: + pass diff --git a/tum_esm_utils/code.py b/tum_esm_utils/code.py index a3bc30e..5a05cd5 100644 --- a/tum_esm_utils/code.py +++ b/tum_esm_utils/code.py @@ -15,14 +15,14 @@ def request_github_file( ) -> str: """Sends a request and returns the content of the response, as a string. Raises an HTTPError if the response status code is not 200. - + Args: repository: In the format "owner/repo". filepath: The path to the file in the repository. access_token: The GitHub access token. Only required if the repo is private. branch_name: The branch name. timeout: The request timeout in seconds. - + Returns: The content of the file as a string. """ @@ -53,7 +53,7 @@ def request_gitlab_file( ) -> str: """Sends a request and returns the content of the response, as a string. Raises an HTTPError if the response status code is not 200. - + Args: repository: In the format "owner/repo". filepath: The path to the file in the repository. @@ -61,7 +61,7 @@ def request_gitlab_file( branch_name: The branch name. hostname: The GitLab hostname. timeout: The request timeout in seconds. - + Returns: The content of the file as a string. """ diff --git a/tum_esm_utils/datastructures.py b/tum_esm_utils/datastructures.py index 4a97d11..c533375 100644 --- a/tum_esm_utils/datastructures.py +++ b/tum_esm_utils/datastructures.py @@ -37,8 +37,8 @@ def get(self) -> list[float]: if self._current_index >= (self._max_size - 1): bounded_current_index = self._current_index % self._max_size return ( - self._data[bounded_current_index + 1 : self._max_size + 1] + - self._data[0 : bounded_current_index + 1] + self._data[bounded_current_index + 1 : self._max_size + 1] + + self._data[0 : bounded_current_index + 1] ) # list is not empty but not full diff --git a/tum_esm_utils/decorators.py b/tum_esm_utils/decorators.py index b1710df..09c5d31 100644 --- a/tum_esm_utils/decorators.py +++ b/tum_esm_utils/decorators.py @@ -17,16 +17,17 @@ class with_filelock: wait until other programs are done using it. See https://en.wikipedia.org/wiki/Semaphore_(programming). - - + + Credits for the typing of higher level decorators goes to https://github.com/python/mypy/issues/1551#issuecomment-253978622. """ + def __init__(self, lockfile_path: str, timeout: float = -1) -> None: """Create a new filelock decorator. - + A timeout of -1 means that the code waits forever. - + Args: lockfile_path: The path to the lockfile. timeout: The time to wait for the lock in seconds.""" diff --git a/tum_esm_utils/em27.py b/tum_esm_utils/em27.py index e0bbbc4..adf3ae3 100644 --- a/tum_esm_utils/em27.py +++ b/tum_esm_utils/em27.py @@ -2,12 +2,12 @@ Implements: `detect_corrupt_opus_files`, `load_proffast2_result`. -This requires you to install this utils library with the optional `polars` dependency: +This requires you to install this utils library with the optional `em27` dependency: ```bash -pip install "tum_esm_utils[polars]" +pip install "tum_esm_utils[em27]" # or -pdm add "tum_esm_utils[polars]" +pdm add "tum_esm_utils[em27]" ```""" from __future__ import annotations @@ -20,9 +20,7 @@ import polars as pl from tailwind_colors import TAILWIND_COLORS_HEX as TCH -_PARSER_DIR = os.path.join( - os.path.dirname(os.path.abspath(__file__)), "opus_file_validator" -) +_PARSER_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "opus_file_validator") def _compile_fortran_code( @@ -30,15 +28,13 @@ def _compile_fortran_code( fortran_compiler: Literal["gfortran", "gfortran-9"] = "gfortran", force_recompile: bool = False, ) -> None: - if force_recompile or ( - not os.path.isfile(os.path.join(_PARSER_DIR, "opus_file_validator")) - ): + if force_recompile or (not os.path.isfile(os.path.join(_PARSER_DIR, "opus_file_validator"))): if not silent: print("compiling fortran code") command = ( - f"{fortran_compiler} -nocpp -O3 -o ./opus_file_validator " + - f"glob_prepro6.F90 glob_OPUSparms6.F90 opus_file_validator.F90" + f"{fortran_compiler} -nocpp -O3 -o ./opus_file_validator " + + f"glob_prepro6.F90 glob_OPUSparms6.F90 opus_file_validator.F90" ) p = subprocess.run( command, @@ -52,8 +48,8 @@ def _compile_fortran_code( stdout = p.stdout.decode("utf-8", errors="replace").strip() stderr = p.stderr.decode("utf-8", errors="replace").strip() raise Exception( - f"command '{command}' failed with exit code {p.returncode}, " + - f"stderr: {stderr}, stout:{stdout}", + f"command '{command}' failed with exit code {p.returncode}, " + + f"stderr: {stderr}, stout:{stdout}", ) @@ -72,7 +68,7 @@ def detect_corrupt_opus_files( (https://www.imk-asf.kit.edu/english/3225.php). We use it because the retrieval using Proffast 2 will fail if there are corrupt interferograms in the input. - + Args: ifg_directory: The directory containing the interferograms. silent: If set to False, print additional information. @@ -89,18 +85,18 @@ def detect_corrupt_opus_files( timeout=30, ): _compile_fortran_code( - silent=silent, - fortran_compiler=fortran_compiler, - force_recompile=force_recompile + silent=silent, fortran_compiler=fortran_compiler, force_recompile=force_recompile ) # list directory files filepaths = list( - sorted([ - fp for fp in - [f"{ifg_directory}/{x}" for x in os.listdir(ifg_directory)] - if os.path.isfile(fp) - ]) + sorted( + [ + fp + for fp in [f"{ifg_directory}/{x}" for x in os.listdir(ifg_directory)] + if os.path.isfile(fp) + ] + ) ) # write input file for parser in a semaphore @@ -118,9 +114,7 @@ def detect_corrupt_opus_files( with open(f"{_PARSER_DIR}/opus_file_validator.template.inp", "r") as f: template_content = f.read() with open(input_file_path, "w") as f: - f.write( - template_content.replace("%IFG_LIST%", "\n".join(filepaths)) - ) + f.write(template_content.replace("%IFG_LIST%", "\n".join(filepaths))) # run the parser process = subprocess.run( @@ -135,17 +129,20 @@ def detect_corrupt_opus_files( if not process.returncode == 0: raise RuntimeError( - f"Opus File Parser failed with exit code {process.returncode}, " + - f"stderr: {stderr}, stdout: {stdout}", + f"Opus File Parser failed with exit code {process.returncode}, " + + f"stderr: {stderr}, stdout: {stdout}", ) # locate the block of verification results - if ((stdout.count("--- Start verifying file integrities ---") != 1) or - (stdout.count("--- Done verifying file integrities ---") != 1)): + if (stdout.count("--- Start verifying file integrities ---") != 1) or ( + stdout.count("--- Done verifying file integrities ---") != 1 + ): raise Exception("This is a bug in the `tum_esm_utils` library") - verification_block = stdout.split( - "--- Start verifying file integrities ---" - )[1].split("--- Done verifying file integrities ---")[0].strip("\t\n ") + verification_block = ( + stdout.split("--- Start verifying file integrities ---")[1] + .split("--- Done verifying file integrities ---")[0] + .strip("\t\n ") + ) # parse the verification results results: dict[str, list[str]] = {} @@ -157,14 +154,12 @@ def detect_corrupt_opus_files( is_corrupt = len(lines) > 2 filepath = lines[0].split('"')[1] if is_corrupt: - results[os.path.basename(filepath)] = lines[1 :-1] + results[os.path.basename(filepath)] = lines[1:-1] checked_files.remove(filepath) # every file not mentioned in the verification results failed during reading it for filepath in checked_files: - results[os.path.basename(filepath)] = [ - "File not even readible by the parser" - ] + results[os.path.basename(filepath)] = ["File not even readible by the parser"] # save the raw output for debugging purposes with open(os.path.join(_PARSER_DIR, "output.txt"), "w") as f: @@ -174,8 +169,8 @@ def detect_corrupt_opus_files( @deprecated( - "This will be removed in the next breaking release. Please use " + - "the identical function `detect_corrupt_opus_files` instead." + "This will be removed in the next breaking release. Please use " + + "the identical function `detect_corrupt_opus_files` instead." ) def detect_corrupt_ifgs( ifg_directory: str, @@ -192,7 +187,7 @@ def detect_corrupt_ifgs( (https://www.imk-asf.kit.edu/english/3225.php). We use it because the retrieval using Proffast 2 will fail if there are corrupt interferograms in the input. - + Args: ifg_directory: The directory containing the interferograms. silent: If set to False, print additional information. @@ -215,16 +210,18 @@ def load_proffast2_result(path: str) -> pl.DataFrame: Args: path: The path to the Proffast 2 output file. - + Returns: A polars DataFrame containing all columns. """ + # fmt: off column_names = [ "JulianDate", "UTtimeh", "gndP", "gndT", "latdeg", "londeg", "altim", "appSZA", "azimuth", "XH2O", "XAIR", "XCO2", "XCH4", "XCO2_STR", "XCO", "XCH4_S5P", "H2O", "O2", "CO2", "CH4", "CO", "CH4_S5P" ] + # fmt: on df = pl.read_csv( path, has_header=True, @@ -233,19 +230,17 @@ def load_proffast2_result(path: str) -> pl.DataFrame: "UTC": pl.Datetime, " LocalTime": pl.Utf8, " spectrum": pl.Utf8, - **{f" {cn}": pl.Float32 - for cn in column_names}, - } + **{f" {cn}": pl.Float32 for cn in column_names}, + }, ).drop(" JulianDate", " UTtimeh") - return df.rename({ - " LocalTime": "LocalTime", - " spectrum": "spectrum", - **{f" {cn}": cn - for cn in column_names if f" {cn}" in df.columns}, - }).with_columns( - pl.col("LocalTime").str.strptime( - dtype=pl.Datetime, format=" %Y-%m-%d %H:%M:%S" - ), + return df.rename( + { + " LocalTime": "LocalTime", + " spectrum": "spectrum", + **{f" {cn}": cn for cn in column_names if f" {cn}" in df.columns}, + } + ).with_columns( + pl.col("LocalTime").str.strptime(dtype=pl.Datetime, format=" %Y-%m-%d %H:%M:%S"), ) @@ -302,12 +297,12 @@ def load_proffast2_result(path: str) -> pl.DataFrame: "XCO": 1000, "XCH4_S5P": 1000, "XAIR": 1, - "H2O": 1 / (6.022 * 10e+23), - "O2": 1 / (6.022 * 10e+23), - "CO2": 1 / (6.022 * 10e+23), - "CH4": 1 / (6.022 * 10e+23), - "CO": 1 / (6.022 * 10e+23), - "CH4_S5P": 1 / (6.022 * 10e+23), + "H2O": 1 / (6.022 * 10e23), + "O2": 1 / (6.022 * 10e23), + "CO2": 1 / (6.022 * 10e23), + "CH4": 1 / (6.022 * 10e23), + "CO": 1 / (6.022 * 10e23), + "CH4_S5P": 1 / (6.022 * 10e23), } """Multiplication factors for the EM27 data retrieved using Proffast to bring the data in a common unit.""" diff --git a/tum_esm_utils/files.py b/tum_esm_utils/files.py index 2c0ad02..087e78f 100644 --- a/tum_esm_utils/files.py +++ b/tum_esm_utils/files.py @@ -17,21 +17,37 @@ def load_file(path: str) -> str: + """Load the content of a file.""" with open(path, "r") as f: return f.read() def dump_file(path: str, content: str) -> None: + """Dump content to a file.""" with open(path, "w") as f: f.write(content) +def load_binary_file(path: str) -> bytes: + """Load binary content of a file.""" + with open(path, "rb") as f: + return f.read() + + +def dump_binary_file(path: str, content: bytes) -> None: + """Dump binary content to a file.""" + with open(path, "wb") as f: + f.write(content) + + def load_json_file(path: str) -> Any: + """Load the content of a JSON file.""" with open(path, "r") as f: return json.load(f) def dump_json_file(path: str, content: Any, indent: Optional[int] = 4) -> None: + """Dump content to a JSON file.""" with open(path, "w") as f: json.dump(content, f, indent=indent) @@ -53,9 +69,7 @@ def get_dir_checksum(path: str) -> str: """Get the checksum of a directory using md5deep.""" assert os.path.isdir(path), f"{path} is not a directory" - return tum_esm_utils.shell.run_shell_command( - f"md5deep -r -b {path} | sort -u | md5sum" - ) + return tum_esm_utils.shell.run_shell_command(f"md5deep -r -b {path} | sort -u | md5sum") def get_file_checksum(path: str) -> str: @@ -71,11 +85,11 @@ def get_file_checksum(path: str) -> str: def rel_to_abs_path(*path: str) -> str: """Convert a path relative to the caller's file to an absolute path. - + Inside file `/home/somedir/somepath/somefile.py`, calling `rel_to_abs_path("..", "config", "config.json")` will return `/home/somedir/config/config.json`. - + Credits to https://stackoverflow.com/a/59004672/8255842""" return os.path.normpath( @@ -95,7 +109,7 @@ def read_last_n_lines( The function returns less than `n` lines if the file has less than `n` lines. The last element in the list is the last line of the file. - + This function uses seeking in order not to read the full file. The simple approach of reading the last 10 lines would be: @@ -106,7 +120,7 @@ def read_last_n_lines( However, this would read the full file and if we only need to read 10 lines out of a 2GB file, this would be a big waste of resources. - + The `ignore_trailing_whitespace` option to crop off trailing whitespace, i.e. only return the last `n` lines that are not empty or only contain whitespace.""" @@ -151,7 +165,7 @@ def expect_file_contents( ) -> None: """Assert that the given file contains all of the required content blocks, and/or none of the forbidden content blocks. - + Args: filepath: The path to the file. required_content_blocks: A list of strings that must be present in the file. @@ -177,9 +191,9 @@ def render_directory_tree( file_prefix: Optional[str] = "📄 ", ) -> Optional[str]: """Render a file tree as a string. - + Example: - + ``` 📁 ├─── 📁 bundle @@ -198,7 +212,7 @@ def render_directory_tree( │ ├─── 📁 algorithms ... ``` - + Args: root: The root directory to render. ignore: A list of patterns to ignore. If the basename of a directory @@ -209,13 +223,11 @@ def render_directory_tree( directory is was aliased to ``. directory_prefix: The prefix to use for directories. file_prefix: The prefix to use for files. - + Returns: The directory tree as a string. If the root directory is ignored, `None`. """ - if any([ - fnmatch.fnmatch(os.path.basename(root), pattern) for pattern in ignore - ]): + if any([fnmatch.fnmatch(os.path.basename(root), pattern) for pattern in ignore]): return None root_name = os.path.basename(root) if root_alias is None else root_alias @@ -235,7 +247,7 @@ def render_directory_tree( sd = render_directory_tree( os.path.join(root, item), ignore=ignore, - max_depth=(max_depth - 1) if max_depth is not None else None + max_depth=(max_depth - 1) if max_depth is not None else None, ) if sd is not None: sublists.append(sd) @@ -272,7 +284,7 @@ def list_directory( """List the contents of a directory based on certain criteria. Like `os.listdir` with superpowers. You can filter the list by a regex or you can ignore Unix shell style patterns like `*.lock`. - + Args: path: The path to the directory. regex: A regex pattern to match the item names against. @@ -281,7 +293,7 @@ def list_directory( include_directories: Whether to include directories in the output. include_files: Whether to include files in the output. include_links: Whether to include symbolic links in the output. - + Returns: A list of items in the directory that match the criteria. """ @@ -290,10 +302,7 @@ def list_directory( if regex is not None: files = [f for f in files if re.match(regex, f)] if ignore is not None: - files = [ - f for f in files - if not any([fnmatch.fnmatch(f, pattern) for pattern in ignore]) - ] + files = [f for f in files if not any([fnmatch.fnmatch(f, pattern) for pattern in ignore])] if not include_directories: files = [f for f in files if not os.path.isdir(os.path.join(path, f))] if not include_files: diff --git a/tum_esm_utils/opus/__init__.py b/tum_esm_utils/opus/__init__.py new file mode 100644 index 0000000..9795168 --- /dev/null +++ b/tum_esm_utils/opus/__init__.py @@ -0,0 +1,170 @@ +"""Functions for interacting with OPUS files. + +Implements: `OpusFile`. + +Read https://tccon-wiki.caltech.edu/Main/I2SAndOPUSHeaders for more information +about the file parameters. This requires you to install this utils library with +the optional `opus` dependency: + +```bash +pip install "tum_esm_utils[opus]" +# or +pdm add "tum_esm_utils[opus]" +``` + +Credits to Friedrich Klappenbach (ge79wul@mytum.de) for decoding the OPUS file +format.""" + +from __future__ import annotations +from typing import Optional, Literal +import numpy as np +import numpy.typing as npt +import datetime +import pydantic + +from . import types, utils + + +class OpusFile(pydantic.BaseModel): + model_config = pydantic.ConfigDict(arbitrary_types_allowed=True) + + header: types.OpusHeader + channel_parameters: list[types.OpusChannelParameters] + measurement_times: list[datetime.datetime] + interferogram: Optional[npt.NDArray[np.float64]] = pydantic.Field(default=None, exclude=True) + + # serialization of measurement times + @pydantic.field_serializer("measurement_times") + def serialize_measurement_times(self, measurement_times: list[datetime.datetime]) -> list[str]: + return [t.isoformat() for t in measurement_times] + + @staticmethod + def read( + filepath: str, + measurement_timestamp_mode: Literal["start", "end"] = "start", + interferogram_mode: Literal["skip", "validate", "read"] = "read", + read_all_channels: bool = True, + ) -> OpusFile: + """Read an interferogram file. + + Args: + filepath: Path to the OPUS file. + measurement_timestamp_mode: Whether the timestamps in the interferograms + indicate the start or end of the measurement + interferogram_mode: How to handle the interferogram data. "skip" + will not read the interferogram data, "validate" + will read the first and last block to check + for errors during writing, "read" will read + the entire interferogram. "read" takes about + 11-12 times longer than "skip", "validate" is + about 20% slower than "skip". + read_all_channels: Whether to read all channels in the file or + only the first one. + + Returns: + An OpusFile object, optionally containing the interferogram data (in read mode) + """ + + # what to copy when only manipulating the interferogram + # 4 (MAGIC) + struct.calcsize('= 1 + ), f"found {len(block_indices['DBTDSTAT'])} DBTDSTAT blocks" + for b in ["DBTINSTR", "DBTAQPAR", "DBTPRCPAR", "DBTFTPAR", "DBTORGPAR"]: + assert len(block_indices[b]) == 1, f"found {len(block_indices[b])} {b} blocks" + + parameters_ch1 = types.OpusChannelParameters.model_validate( + { + k: utils.read_opus_block( + f, opus_dirs[block_indices[k][0]], types.OpusParameterBlock + ).data + for k in [ + "DBTAQPAR", + "DBTORGPAR", + "DBTDSTAT", + "DBTINSTR", + "DBTPRCPAR", + "DBTFTPAR", + ] + } + ) + + channel_count = len(block_indices["DBTDSTAT"]) + assert ( + len(block_indices["interferogram"]) == channel_count + ), f"found {len(block_indices['interferogram'])} interferogram blocks, but {channel_count} DBTDSTAT blocks" + read_channel_count = channel_count if read_all_channels else 1 + + # all channels share the same parameters, except for the spectrum + channel_parameters = [parameters_ch1] + for i in range(1, read_channel_count): + p = parameters_ch1.model_copy(deep=True) + p.spectrum = utils.read_opus_block( + f, + opus_dirs[block_indices["DBTDSTAT"][i]], + types.OpusParameterBlock, + ).data + channel_parameters.append(p) + + interferogram: Optional[npt.NDArray[np.float64]] = None + + # validate = only check if the block is fully present + if interferogram_mode == "validate": + for block_index in block_indices["interferogram"]: + utils.read_opus_block(f, opus_dirs[block_index], types.OpusDataBlock) + + # read = read the entire interferogram + if interferogram_mode == "read": + interferogram = utils.read_interferogram( + f, + channel_parameters=channel_parameters, + ifg_opus_dirs=[opus_dirs[i] for i in block_indices["interferogram"]], + read_all_channels=read_all_channels, + ) + + return OpusFile( + header=opus_header, + channel_parameters=channel_parameters, + measurement_times=[ + p.parse_measurement_datetime(measurement_timestamp_mode) for p in channel_parameters + ], + interferogram=interferogram, + ) diff --git a/tum_esm_utils/opus/types.py b/tum_esm_utils/opus/types.py new file mode 100644 index 0000000..d5dfd61 --- /dev/null +++ b/tum_esm_utils/opus/types.py @@ -0,0 +1,77 @@ +"""Types for interacting with OPUS files. + +Credits to Friedrich Klappenbach (ge79wul@mytum.de) for decoding the OPUS file format.""" + +from __future__ import annotations +from typing import Any, Literal, Optional +import math +import datetime +import pydantic +import tum_esm_utils + + +class OpusHeader(pydantic.BaseModel): + """Object that contains all the information in the OPUS file header block.""" + + version: int = pydantic.Field(..., ge=0, description="File version") + dir_pointer: int = pydantic.Field( + ..., ge=0, description="File pointer to start of OPUS directory" + ) + max_dir_size: int = pydantic.Field(..., ge=0, description="Maximum number of directory entries") + dir_size: int = pydantic.Field(..., ge=0, description="Used number of OPUS directory entries") + + +class OpusParameterBlock(pydantic.BaseModel): + data: dict[str, Optional[str | float | int]] + data_types: dict[str, int] + parameter_order: list[str] + raw_data: bytes + + +class OpusDataBlock(pydantic.BaseModel): + raw_data: bytes + + +class OpusDirectoryEntry(pydantic.BaseModel): + """Object that contains all the information in an OPUS directory entry.""" + + block_type: int = pydantic.Field(..., description="encoded block type (32 bit mask)") + block_length: int = pydantic.Field(..., description="block length (32-bit words)") + block_pointer: int = pydantic.Field( + ..., description="file pointer to beginning of data/parameter block (bytes)" + ) + block_category: str = pydantic.Field( + ..., description="decoded parameter type flag (values from DBSPARM)" + ) + + +class OpusChannelParameters(pydantic.BaseModel): + spectrum: dict[str, Any] = pydantic.Field(..., validation_alias="DBTDSTAT") + instrument: dict[str, Any] = pydantic.Field(..., validation_alias="DBTINSTR") + acquisition: dict[str, Any] = pydantic.Field(..., validation_alias="DBTAQPAR") + optics: dict[str, Any] = pydantic.Field(..., validation_alias="DBTPRCPAR") + sample: dict[str, Any] = pydantic.Field(..., validation_alias="DBTORGPAR") + fourier_transform: dict[str, Any] = pydantic.Field(..., validation_alias="DBTFTPAR") + + def parse_measurement_datetime( + self, + measurement_timestamp_mode: Literal["start", "end"] = "start", + ) -> datetime.datetime: + DAT, TIM, DUR = self.spectrum["DAT"], self.spectrum["TIM"], self.instrument["DUR"] + seconds = float(TIM.split(":")[2].split(" ")[0]) + utc_offset = tum_esm_utils.timing.parse_timezone_string(TIM.split(" ")[-1].strip("()")) + dt = datetime.datetime( + year=int(DAT.split("/")[2]), + month=int(DAT.split("/")[1]), + day=int(DAT.split("/")[0]), + hour=int(TIM.split(":")[0]), + minute=int(TIM.split(":")[1]), + second=math.floor(seconds), + microsecond=round((seconds % 1) * 1_000_000), + tzinfo=datetime.timezone.utc, + ) - datetime.timedelta(hours=utc_offset) + if measurement_timestamp_mode == "start": + dt = dt + datetime.timedelta(seconds=DUR / 2) + else: + dt = dt - datetime.timedelta(seconds=DUR / 2) + return dt diff --git a/tum_esm_utils/opus/utils.py b/tum_esm_utils/opus/utils.py new file mode 100644 index 0000000..fc9c598 --- /dev/null +++ b/tum_esm_utils/opus/utils.py @@ -0,0 +1,196 @@ +"""Credits to Friedrich Klappenbach (ge79wul@mytum.de) for decoding the OPUS file format.""" + +from __future__ import annotations +from typing import Optional, TypeVar +import numpy as np +import numpy.typing as npt +import io +import struct +from . import types + +# Define constants + +FILE_MAGIC = b"\n\n\xfe\xfe" # magic sequence at beginning of Opus file +# Definitions for directory block type bit patterns + +# Parameter type flag +DBSPARM = 4 # bit shift +DBMPARM = 63 # bit mask +DBFPARM: list[str] = [ + "", # undefined + "DBTDSTAT", # data status parameter (DataParameters ) + "DBTINSTR", # instrument status parameters + "DBTAQPAR", # standard acquisition parameters + "DBTFTPAR", # FT-Parameters + "DBTPLTPAR", # plot- and display parameters + "DBTPRCPAR", # processing parameters (Optic parameters) + "DBTGCPAR", # GC-parameters + "DBTLIBPAR", # library search parameters + "DBTCOMPAR", # communication parameters + "DBTORGPAR", # sample origin parameter (Sample Parameters) +] + [f"DBTPARM{i}" for i in range(11, DBMPARM + 1)] +assert len(DBFPARM) == DBMPARM + 1 + + +def read_opus_header(f: io.BufferedReader) -> types.OpusHeader: + """Read OPUS file header from a file.""" + + magic_sequence = f.read(4) + if magic_sequence != FILE_MAGIC: + raise RuntimeError(f'Magic sequence not found, found {magic_sequence.decode()}" instead') + x = struct.unpack("= 4 + return types.OpusHeader( + version=x[0], + dir_pointer=x[1], + max_dir_size=x[2], + dir_size=x[3], + ) + + +def read_opus_dir_entry(f: io.BufferedReader) -> types.OpusDirectoryEntry: + """Read a single OPUS directory entry object from file at current position.""" + + fmt = "> DBSPARM) & DBMPARM], + ) + + +T = TypeVar("T", types.OpusParameterBlock, types.OpusDataBlock) + + +def read_opus_block( + f: io.BufferedReader, + opus_directory_entry: types.OpusDirectoryEntry, + expected_block_type: type[T], +) -> T: + f.seek(opus_directory_entry.block_pointer) + raw_data = f.read(4 * opus_directory_entry.block_length) + + block: types.OpusParameterBlock | types.OpusDataBlock + if opus_directory_entry.block_category == "": + block = types.OpusDataBlock(raw_data=raw_data) + if not isinstance(block, expected_block_type): + raise RuntimeError(f"Expected a {expected_block_type}, but got a {type(block)}") + return block + + data: dict[str, Optional[int | float | str]] = {} + data_types: dict[str, int] = {} + parameter_order: list[str] = [] + + offset = 0 # Byte offset from beginning of block + fmt1 = "<4shh" # Initial format string: 4-byte string, 2 short int (little endian) + while offset <= (len(raw_data) - struct.calcsize(fmt1)): + # rs: seserved space in 16 bit units + (pname, ptype, rs) = struct.unpack_from(fmt1, raw_data, offset) + assert isinstance(pname, bytes) + assert isinstance(ptype, int) + assert isinstance(rs, int) + + parameter_name = pname[:-1].decode("utf-8") # Remove '\0' terminator + offset += struct.calcsize(fmt1) + value: Optional[int | float | str] = None + + # Try to read the following values + if rs > 0: + # INT32 (little endian) + if ptype == 0: + fmt2 = " npt.NDArray[np.float64]: + if len(channel_parameters) != len(ifg_opus_dirs): + raise RuntimeError("Number of channel parameters and interferogram blocks do not match!") + + if len(channel_parameters) == 0: + raise RuntimeError("No channel parameters found!") + + if not read_all_channels: + channel_parameters = channel_parameters[:1] + ifg_opus_dirs = ifg_opus_dirs[:1] + + spectrum_length = channel_parameters[0].spectrum["NPT"] + for channel_parameter in channel_parameters[1:]: + if channel_parameter.spectrum["NPT"] != spectrum_length: + raise RuntimeError("The interferograms don't have the same length!") + + if len(ifg_opus_dirs) not in [1, 2]: + raise RuntimeError(f"Invalid number of interferogram blocks found: {len(ifg_opus_dirs)}") + + for ifg_opus_dir in ifg_opus_dirs: + if ifg_opus_dir.block_length != spectrum_length: + raise RuntimeError( + f"Interferogram block has length {ifg_opus_dir.block_length}, " + f"but expected {spectrum_length}" + ) + + full_ifg = np.zeros( + shape=(len(ifg_opus_dirs), spectrum_length), + dtype=np.float64, + ) + for channel_index in range(len(ifg_opus_dirs)): + channel_ifg = np.multiply( + np.array( + struct.unpack_from( + f"<{spectrum_length}f", + read_opus_block(f, ifg_opus_dirs[0], types.OpusDataBlock).raw_data, + offset=0, + ) + ), + channel_parameters[channel_index].spectrum["CSF"], + ) + full_ifg[channel_index, :] = channel_ifg.copy() + + return full_ifg diff --git a/tum_esm_utils/plotting.py b/tum_esm_utils/plotting.py index f3cd978..6d5c415 100644 --- a/tum_esm_utils/plotting.py +++ b/tum_esm_utils/plotting.py @@ -31,28 +31,24 @@ def apply_better_defaults(font_family: Optional[str] = "Roboto") -> None: system_fonts = matplotlib.font_manager.findSystemFonts() matching_fonts = [font for font in system_fonts if font_family in font] if len(matching_fonts) == 0: - raise ValueError( - f"Font family '{font_family}' not found. System fonts: {system_fonts}" - ) + raise ValueError(f"Font family '{font_family}' not found. System fonts: {system_fonts}") for font in matching_fonts: matplotlib.font_manager.fontManager.addfont(font) - plt.rcParams['font.sans-serif'] = [ - font_family, *plt.rcParams['font.sans-serif'] - ] - - plt.rcParams['figure.titleweight'] = 'bold' - plt.rcParams['axes.titleweight'] = 'semibold' - plt.rcParams['axes.labelweight'] = 'semibold' - plt.rcParams['axes.facecolor'] = TAILWIND_COLORS_HEX.SLATE_050 - plt.rcParams['axes.edgecolor'] = TAILWIND_COLORS_HEX.SLATE_600 - plt.rcParams['grid.color'] = TAILWIND_COLORS_HEX.SLATE_300 - plt.rcParams['axes.axisbelow'] = True - plt.rcParams['xtick.color'] = TAILWIND_COLORS_HEX.SLATE_600 - plt.rcParams['ytick.color'] = TAILWIND_COLORS_HEX.SLATE_600 - plt.rcParams['xtick.labelcolor'] = "black" - plt.rcParams['ytick.labelcolor'] = "black" - plt.rcParams['scatter.edgecolors'] = "none" - matplotlib.style.use('fast') + plt.rcParams["font.sans-serif"] = [font_family, *plt.rcParams["font.sans-serif"]] + + plt.rcParams["figure.titleweight"] = "bold" + plt.rcParams["axes.titleweight"] = "semibold" + plt.rcParams["axes.labelweight"] = "semibold" + plt.rcParams["axes.facecolor"] = TAILWIND_COLORS_HEX.SLATE_050 + plt.rcParams["axes.edgecolor"] = TAILWIND_COLORS_HEX.SLATE_600 + plt.rcParams["grid.color"] = TAILWIND_COLORS_HEX.SLATE_300 + plt.rcParams["axes.axisbelow"] = True + plt.rcParams["xtick.color"] = TAILWIND_COLORS_HEX.SLATE_600 + plt.rcParams["ytick.color"] = TAILWIND_COLORS_HEX.SLATE_600 + plt.rcParams["xtick.labelcolor"] = "black" + plt.rcParams["ytick.labelcolor"] = "black" + plt.rcParams["scatter.edgecolors"] = "none" + matplotlib.style.use("fast") @contextlib.contextmanager @@ -66,7 +62,7 @@ def create_figure( dpi: int = 250, ) -> Generator[plt.Figure, None, None]: """Create a figure for plotting. - + Usage: ```python @@ -103,7 +99,7 @@ def add_subplot( **kwargs: dict[str, Any], ) -> plt.Axes: """Add a subplot to a figure. - + Args: fig: The figure to add the subplot to. position: The position of the subplot. The tuple should contain three integers: the number of rows, the number of columns, and the index of the subplot. @@ -111,10 +107,10 @@ def add_subplot( xlabel: The x-axis label of the subplot. ylabel: The y-axis label of the subplot. **kwargs: Additional keyword arguments for the subplot. - + Returns: An axis object for the new subplot. - + Raises: ValueError: If the index of the subplot is invalid.""" @@ -137,19 +133,24 @@ def add_subplot( def add_colorpatch_legend( fig: plt.Figure, - handles: list[tuple[str, Union[ - str, - tuple[float, float, float], - tuple[float, float, float, float], - ]]], + handles: list[ + tuple[ + str, + Union[ + str, + tuple[float, float, float], + tuple[float, float, float, float], + ], + ] + ], ncols: Optional[int] = None, location: str = "upper left", ) -> None: """Add a color patch legend to a figure. - + Args: fig: The figure to add the legend to. - handles: A list of tuples containing the label and color of each patch + handles: A list of tuples containing the label and color of each patch (e.g. `[("Label 1", "red"), ("Label 2", "blue")]`). You can pass any color that is accepted by matplotlib. ncols: The number of columns in the legend. @@ -157,10 +158,7 @@ def add_colorpatch_legend( """ fig.legend( - handles=[ - matplotlib.patches.Patch(color=color, label=label) - for label, color in handles - ], + handles=[matplotlib.patches.Patch(color=color, label=label) for label, color in handles], ncol=len(handles) if ncols is None else ncols, loc=location, ) diff --git a/tum_esm_utils/processes.py b/tum_esm_utils/processes.py index 74e41b3..5f91983 100644 --- a/tum_esm_utils/processes.py +++ b/tum_esm_utils/processes.py @@ -12,7 +12,7 @@ def get_process_pids(script_path: str) -> list[int]: """Return a list of PIDs that have the given script as their entrypoint. - + Args: script_path: The absolute path of the python file entrypoint.""" @@ -32,20 +32,18 @@ def get_process_pids(script_path: str) -> list[int]: def start_background_process( - interpreter_path: str, - script_path: str, - waiting_period: float = 0.5 + interpreter_path: str, script_path: str, waiting_period: float = 0.5 ) -> int: """Start a new background process with nohup with a given python interpreter and script path. The script paths parent directory will be used as the working directory for the process. - + Args: interpreter_path: The absolute path of the python interpreter. script_path: The absolute path of the python file entrypoint. waiting_period: The waiting period in seconds after starting the process. - + Returns: The PID of the started process. """ @@ -70,10 +68,10 @@ def terminate_process( ) -> list[int]: """Terminate all processes that have the given script as their entrypoint. Returns the list of terminated PIDs. - + If `termination_timeout` is not None, the processes will be terminated forcefully after the given timeout (in seconds). - + Args: script_path: The absolute path of the python file entrypoint. termination_timeout: The timeout in seconds after which the diff --git a/tum_esm_utils/shell.py b/tum_esm_utils/shell.py index 37ee27b..55cc0e9 100644 --- a/tum_esm_utils/shell.py +++ b/tum_esm_utils/shell.py @@ -13,6 +13,7 @@ class CommandLineException(Exception): """Exception raised for errors in the command line.""" + def __init__(self, value: str, details: Optional[str] = None) -> None: self.value = value self.details = details @@ -30,12 +31,12 @@ def run_shell_command( """runs a shell command and raises a `CommandLineException` if the return code is not zero, returns the stdout. Uses `/bin/bash` by default. - + Args: command: The command to run. working_directory: The working directory for the command. executable: The shell executable to use. - + Returns: The stdout of the command as a string.""" @@ -69,22 +70,19 @@ def get_hostname() -> str: return (raw.split(".")[0]) if ("." in raw) else raw -def get_commit_sha( - variant: Literal["short", "long"] = "short" -) -> Optional[str]: +def get_commit_sha(variant: Literal["short", "long"] = "short") -> Optional[str]: """Get the current commit sha of the repository. Returns `None` if there is not git repository in any parent directory. - + Args: variant: "short" or "long" to specify the length of the sha. - + Returns: The commit sha as a string, or `None` if there is no git repository in the parent directories.""" p = subprocess.run( - ["git", "rev-parse", "--verify", "HEAD"] + - (["--short"] if variant == "short" else []), + ["git", "rev-parse", "--verify", "HEAD"] + (["--short"] if variant == "short" else []), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) @@ -103,22 +101,19 @@ def change_file_permissions(file_path: str, permission_string: str) -> None: """Change a file's system permissions. Example permission_strings: `--x------`, `rwxr-xr-x`, `rw-r--r--`. - + Args: file_path: The path to the file. permission_string: The new permission string.""" - assert _permission_string_pattern.match( - permission_string - ), "Invalid permission string" + assert _permission_string_pattern.match(permission_string), "Invalid permission string" - permission_str_to_bit: Callable[[str], int] = lambda p: sum([ - int(c) for c in p.replace("r", "4").replace("w", "2").replace("x", "1"). - replace("-", "0") - ]) + permission_str_to_bit: Callable[[str], int] = lambda p: sum( + [int(c) for c in p.replace("r", "4").replace("w", "2").replace("x", "1").replace("-", "0")] + ) os.chmod( file_path, - 64 * permission_str_to_bit(permission_string[: 3]) + - 8 * permission_str_to_bit(permission_string[3 : 6]) + - permission_str_to_bit(permission_string[6 :]), + 64 * permission_str_to_bit(permission_string[:3]) + + 8 * permission_str_to_bit(permission_string[3:6]) + + permission_str_to_bit(permission_string[6:]), ) diff --git a/tum_esm_utils/system.py b/tum_esm_utils/system.py index 2c087f4..4e3afea 100644 --- a/tum_esm_utils/system.py +++ b/tum_esm_utils/system.py @@ -44,7 +44,7 @@ def get_disk_space(path: str = "/") -> float: def get_system_battery() -> Optional[int]: """Checks the system battery. - + Returns: The battery state in percent if available, else None.""" @@ -72,9 +72,9 @@ def get_utc_offset() -> float: x = get_utc_offset() local time == utc time + x ``` - + Credits to https://stackoverflow.com/a/35058476/8255842 - + Returns: The UTC offset in hours.""" diff --git a/tum_esm_utils/text.py b/tum_esm_utils/text.py index 871cc9b..edbe172 100644 --- a/tum_esm_utils/text.py +++ b/tum_esm_utils/text.py @@ -14,11 +14,11 @@ def get_random_string(length: int, forbidden: list[str] = []) -> str: """Return a random string from lowercase letters. - + Args: length: The length of the random string. forbidden: A list of strings that should not be generated. - + Returns: A random string.""" @@ -43,7 +43,7 @@ def pad_string( min_width: The minimum width of the text. pad_position: The position of the padding. Either "left" or "right". fill_char: The character to use for padding. - + Returns: The padded string.""" @@ -88,24 +88,24 @@ def insert_replacements(content: str, replacements: dict[str, str]) -> str: """Characters replaced by their ASCII counterparts in `simplify_string_characters`.""" SIMPLE_STRING_REPLACEMENTS: dict[str, str] = { - 'ö': 'oe', - 'ø': 'o', - 'ä': 'ae', - 'å': 'a', - 'ü': 'ue', - 'ß': 'ss', + "ö": "oe", + "ø": "o", + "ä": "ae", + "å": "a", + "ü": "ue", + "ß": "ss", ",": "", - 'é': 'e', - 'ë': 'e', + "é": "e", + "ë": "e", ":": "-", "(": "-", ")": "-", - 'š': "s", - '.': "-", + "š": "s", + ".": "-", "/": "-", - 'ó': "o", - 'ð': "d", - 'á': "a", + "ó": "o", + "ð": "d", + "á": "a", "–": "-", "ł": "l", "‐": "-", @@ -142,14 +142,14 @@ def simplify_string_characters( ) -> str: """Simplify a string by replacing special characters with their ASCII counterparts and removing unwanted characters. - + For example, `simplify_string_characters("Héllo, wörld!")` will return `"hello-woerld"`. - + Args: s: The string to simplify. - additional_replacements: A dictionary of additional replacements to apply. + additional_replacements: A dictionary of additional replacements to apply. `{ "ö": "oe" }` will replace `ö` with `oe`. - + Returns: The simplified string. """ @@ -166,22 +166,18 @@ def simplify_string_characters( allowed_chars = "abcdefghijklmnopqrstuvwxyz0123456789-_@" dirty = [c for c in s if c not in allowed_chars] if len(dirty) > 0: - raise Exception( - f"Found invalid non-replaced characters in name: {dirty} ({s})" - ) + raise Exception(f"Found invalid non-replaced characters in name: {dirty} ({s})") return s -def replace_consecutive_characters( - s: str, characters: list[str] = [" ", "-"] -) -> str: +def replace_consecutive_characters(s: str, characters: list[str] = [" ", "-"]) -> str: """Replace consecutiv characters in a string (e.g. "hello---world" -> "hello-world" or "hello world" -> "hello world"). - + Args: s: The string to process. characters: A list of characters to replace duplicates of. - + Returns: The string with duplicate characters replaced. """ @@ -192,6 +188,7 @@ def replace_consecutive_characters( return s +# fmt: off CONTAINER_ADJECTIVES = set([ "admiring", "adoring", "affectionate", "agitated", "amazing", "angry", "awesome", "beautiful", "blissful", "bold", "boring", "brave", "busy", @@ -250,29 +247,31 @@ def replace_consecutive_characters( "wescoff", "wilbur", "wiles", "williams", "williamson", "wilson", "wing", "wozniak", "wright", "wu", "yalow", "yonath", "zhukovsky" ]) +# fmt: on class RandomLabelGenerator: """A class to generate random labels that follow the Docker style naming of containers, e.g `admiring-archimedes` or `happy-tesla`. - + **Usage with tracking duplicates:** - + ```python generator = RandomLabelGenerator() label = generator.generate() another_label = generator.generate() # Will not be the same as `label` generator.free(label) # Free the label to be used again ``` - + **Usage without tracking duplicates:** - + ```python label = RandomLabelGenerator.generate_fully_random() ``` - + Source for the names and adjectives: https://github.com/moby/moby/blob/master/pkg/namesgenerator/names-generator.go """ + def __init__( self, occupied_labels: set[str] | list[str] = set(), @@ -286,9 +285,7 @@ def __init__( self.adjectives = set(adjectives) self.names = set(names) for label in occupied_labels: - assert re.match( - r"^[a-z]+-[a-z]+$", label - ), f"Invalid label: {label}" + assert re.match(r"^[a-z]+-[a-z]+$", label), f"Invalid label: {label}" adjective, name = label.split("-") assert adjective in adjectives, f"Invalid adjective: {adjective}" assert name in names, f"Invalid name: {name}" @@ -297,23 +294,18 @@ def __init__( def generate(self) -> str: """Generate a random label that is not already occupied.""" - if len(self.occupied_labels - ) == (len(self.adjectives) * len(self.names)): + if len(self.occupied_labels) == (len(self.adjectives) * len(self.names)): raise RuntimeError("All possible labels are used") - min_usage_count = min( - self.adjective_usage_counts.items(), key=lambda x: x[1] - )[1] + min_usage_count = min(self.adjective_usage_counts.items(), key=lambda x: x[1])[1] available_adjectives = [ - a for a, c in self.adjective_usage_counts.items() - if c == min_usage_count + a for a, c in self.adjective_usage_counts.items() if c == min_usage_count ] random_adjective = random.choice(available_adjectives) - used_names = set([ - x.split("-")[1] for x in self.occupied_labels - if x.startswith(random_adjective + "-") - ]) + used_names = set( + [x.split("-")[1] for x in self.occupied_labels if x.startswith(random_adjective + "-")] + ) random_name = random.choice(list(self.names - used_names)) new_label = f"{random_adjective}-{random_name}" @@ -335,7 +327,7 @@ def generate_fully_random( names: set[str] | list[str] = CONTAINER_NAMES, ) -> str: """Get a random label without tracking duplicates. - + Use an instance of `RandomLabelGenerator` if you want to avoid duplicates by tracking occupied labels.""" diff --git a/tum_esm_utils/timing.py b/tum_esm_utils/timing.py index 90fbf44..9aa9dc2 100644 --- a/tum_esm_utils/timing.py +++ b/tum_esm_utils/timing.py @@ -20,9 +20,7 @@ def date_range( """Returns a list of dates between from_date and to_date (inclusive).""" delta = to_date - from_date assert delta.days >= 0, "from_date must be before to_date" - return [ - from_date + datetime.timedelta(days=i) for i in range(delta.days + 1) - ] + return [from_date + datetime.timedelta(days=i) for i in range(delta.days + 1)] @contextlib.contextmanager @@ -48,10 +46,9 @@ def set_alarm(timeout: int, label: str) -> None: """Set an alarm that will raise a `TimeoutError` after `timeout` seconds. The message will be formatted as `{label} took too long (timed out after {timeout} seconds)`.""" + def alarm_handler(*args: Any) -> None: - raise TimeoutError( - f"{label} took too long (timed out after {timeout} seconds)" - ) + raise TimeoutError(f"{label} took too long (timed out after {timeout} seconds)") signal.signal(signal.SIGALRM, alarm_handler) signal.alarm(timeout) @@ -89,15 +86,11 @@ def parse_timezone_string( zone_string: str # parse the offset string - number_of_offset_signs = timezone_string.count("+") + timezone_string.count( - "-" - ) + number_of_offset_signs = timezone_string.count("+") + timezone_string.count("-") if number_of_offset_signs == 0: zone_string = timezone_string elif number_of_offset_signs == 1: - s2 = timezone_string.split( - "+" - ) if "+" in timezone_string else timezone_string.split("-") + s2 = timezone_string.split("+") if "+" in timezone_string else timezone_string.split("-") assert len(s2) == 2 zone_string = s2[0] offset_string = s2[1] @@ -106,10 +99,9 @@ def parse_timezone_string( if re.match(r"^\d{1,2}(\.\d{1})?$", offset_string): offset = float(offset_string) elif re.match(r"^\d{2}:\d{2}$", offset_string): - offset = float(offset_string.split(":")[0] - ) + float(offset_string.split(":")[1]) / 60 + offset = float(offset_string.split(":")[0]) + float(offset_string.split(":")[1]) / 60 elif re.match(r"^\d{4}$", offset_string): - offset = float(offset_string[: 2]) + float(offset_string[2 :]) / 60 + offset = float(offset_string[:2]) + float(offset_string[2:]) / 60 else: raise ValueError(f'Invalid offset string: "{offset_string}"') if "-" in timezone_string: @@ -125,7 +117,9 @@ def parse_timezone_string( f'Unknown time zone: "{zone_string}", the available time zones are: {pytz.all_timezones}' ) td = tz.utcoffset(dt) - assert td is not None, f'Zone "{zone_string}" requires a datetime object to calculate the offset.' + assert ( + td is not None + ), f'Zone "{zone_string}" requires a datetime object to calculate the offset.' offset += td.total_seconds() / 3600 return offset @@ -139,7 +133,7 @@ def wait_for_condition( """Wait for the given condition to be true, or raise a TimeoutError if the condition is not met within the given timeout. The condition is passed as a function that will be called periodically. - + Args: is_successful: A function that returns True if the condition is met. timeout_message: The message to include in the TimeoutError. @@ -155,30 +149,20 @@ def wait_for_condition( time.sleep(check_interval_seconds) -_ISO_8601_PATTERN_1 = re.compile( - r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?$" -) -_ISO_8601_PATTERN_2 = re.compile( - r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$" -) -_ISO_8601_PATTERN_3 = re.compile( - r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(\+|-)\d{2}:\d{2}$" -) -_ISO_8601_PATTERN_4 = re.compile( - r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(\+|-)\d{4}$" -) -_ISO_8601_PATTERN_5 = re.compile( - r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(\+|-)\d{2}$" -) +_ISO_8601_PATTERN_1 = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?$") +_ISO_8601_PATTERN_2 = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$") +_ISO_8601_PATTERN_3 = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(\+|-)\d{2}:\d{2}$") +_ISO_8601_PATTERN_4 = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(\+|-)\d{4}$") +_ISO_8601_PATTERN_5 = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(\+|-)\d{2}$") def parse_iso_8601_datetime(s: str) -> datetime.datetime: """Parse a datetime string from various formats and return a datetime object. - + ISO 8601 supports time zones as `