From 942f81d1dc5670c9c94ea0f45b14420983b81fbb Mon Sep 17 00:00:00 2001 From: Dmitry Shutov Date: Mon, 24 Feb 2025 13:38:54 +0700 Subject: [PATCH 01/23] Move python examples to appropriate folder --- {python => examples/python}/Pipfile | 0 {python => examples/python}/Pipfile.lock | 0 {python => examples/python}/__init__.py | 0 {python => examples/python}/bs_repeatable.py | 0 {python => examples/python}/pydian_repeatable.py | 0 {python => examples/python}/pyproject.toml | 0 {python => examples/python}/resources.py | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename {python => examples/python}/Pipfile (100%) rename {python => examples/python}/Pipfile.lock (100%) rename {python => examples/python}/__init__.py (100%) rename {python => examples/python}/bs_repeatable.py (100%) rename {python => examples/python}/pydian_repeatable.py (100%) rename {python => examples/python}/pyproject.toml (100%) rename {python => examples/python}/resources.py (100%) diff --git a/python/Pipfile b/examples/python/Pipfile similarity index 100% rename from python/Pipfile rename to examples/python/Pipfile diff --git a/python/Pipfile.lock b/examples/python/Pipfile.lock similarity index 100% rename from python/Pipfile.lock rename to examples/python/Pipfile.lock diff --git a/python/__init__.py b/examples/python/__init__.py similarity index 100% rename from python/__init__.py rename to examples/python/__init__.py diff --git a/python/bs_repeatable.py b/examples/python/bs_repeatable.py similarity index 100% rename from python/bs_repeatable.py rename to examples/python/bs_repeatable.py diff --git a/python/pydian_repeatable.py b/examples/python/pydian_repeatable.py similarity index 100% rename from python/pydian_repeatable.py rename to examples/python/pydian_repeatable.py diff --git a/python/pyproject.toml b/examples/python/pyproject.toml similarity index 100% rename from python/pyproject.toml rename to examples/python/pyproject.toml diff --git a/python/resources.py b/examples/python/resources.py similarity index 100% rename from python/resources.py rename to examples/python/resources.py From 61c001fd079b9c650935c1b92ed9f4ceb6c4b08b Mon Sep 17 00:00:00 2001 From: Dmitry Shutov Date: Mon, 24 Feb 2025 14:32:33 +0700 Subject: [PATCH 02/23] Add initial config for the python package --- python/README.md | 1 + python/pyproject.toml | 50 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 python/README.md create mode 100644 python/pyproject.toml diff --git a/python/README.md b/python/README.md new file mode 100644 index 0000000..1072d05 --- /dev/null +++ b/python/README.md @@ -0,0 +1 @@ +# FHIRPathMappingLanguage \ No newline at end of file diff --git a/python/pyproject.toml b/python/pyproject.toml new file mode 100644 index 0000000..ba7b5c1 --- /dev/null +++ b/python/pyproject.toml @@ -0,0 +1,50 @@ +[project] +name = "fhirpath-mapping-language" +description = "The FHIRPath mapping language is a data DSL designed to convert data from QuestionnaireResponse (and not only) to any FHIR Resource." +authors = [ + {name = "Beda Software",email = "ilya@beda.software"} +] +maintainers = [ + { name = "Vadim Laletin", email = "vadim@beda.software" }, + { name = "Ilya Beda", email = "ilya@beda.software" } +] +readme = "README.md" +license = { text = "BSD-3-Clause" } +requires-python = ">=3.9" +keywords = ["fhir", "fhirpath"] +dynamic = ["version", "classifiers"] +dependencies = [ +] + +[project.urls] +homepage = "https://github.com/beda-software/FHIRPathMappingLanguage/tree/main/python/" +repository = "https://github.com/beda-software/FHIRPathMappingLanguage/tree/main/python" +documentation = "https://python-poetry.org/docs/" +"Bug Tracker" = "https://github.com/python-poetry/poetry/issues" + +[tool.poetry] +version = "0.1.0" +packages = [{include = "python", from = "src"}] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Software Development :: Libraries :: Python Modules", + +] + +[tool.poetry.dependencies] +python = ">=3.9,<4.0" + + +[build-system] +requires = ["poetry-core>=2.0.0,<3.0.0"] +build-backend = "poetry.core.masonry.api" From a6009104c3b0c274f2763876c8fd96348ce6fc13 Mon Sep 17 00:00:00 2001 From: Dmitry Shutov Date: Mon, 24 Feb 2025 14:34:39 +0700 Subject: [PATCH 03/23] Adjust project URLs --- python/pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/pyproject.toml b/python/pyproject.toml index ba7b5c1..c68cf52 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -19,8 +19,8 @@ dependencies = [ [project.urls] homepage = "https://github.com/beda-software/FHIRPathMappingLanguage/tree/main/python/" repository = "https://github.com/beda-software/FHIRPathMappingLanguage/tree/main/python" -documentation = "https://python-poetry.org/docs/" -"Bug Tracker" = "https://github.com/python-poetry/poetry/issues" +documentation = "https://github.com/beda-software/FHIRPathMappingLanguage/tree/main/python/README.md" +"Bug Tracker" = "https://github.com/beda-software/FHIRPathMappingLanguage/issues" [tool.poetry] version = "0.1.0" From cab982bf783893669e063f5984e317f59eb51edc Mon Sep 17 00:00:00 2001 From: Dmitry Shutov Date: Mon, 24 Feb 2025 14:58:25 +0700 Subject: [PATCH 04/23] Prepare the project structure --- python/.gitignore | 1 + python/pyproject.toml | 14 +++++--------- python/src/fhirpath-mapping-language/.gitkeep | 0 3 files changed, 6 insertions(+), 9 deletions(-) create mode 100644 python/.gitignore create mode 100644 python/src/fhirpath-mapping-language/.gitkeep diff --git a/python/.gitignore b/python/.gitignore new file mode 100644 index 0000000..6a4cfb4 --- /dev/null +++ b/python/.gitignore @@ -0,0 +1 @@ +./poetry.lock \ No newline at end of file diff --git a/python/pyproject.toml b/python/pyproject.toml index c68cf52..4e9196f 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -1,20 +1,16 @@ [project] name = "fhirpath-mapping-language" description = "The FHIRPath mapping language is a data DSL designed to convert data from QuestionnaireResponse (and not only) to any FHIR Resource." -authors = [ - {name = "Beda Software",email = "ilya@beda.software"} -] +authors = [{ name = "Beda Software", email = "ilya@beda.software" }] maintainers = [ { name = "Vadim Laletin", email = "vadim@beda.software" }, - { name = "Ilya Beda", email = "ilya@beda.software" } + { name = "Ilya Beda", email = "ilya@beda.software" }, ] -readme = "README.md" license = { text = "BSD-3-Clause" } requires-python = ">=3.9" keywords = ["fhir", "fhirpath"] dynamic = ["version", "classifiers"] -dependencies = [ -] +dependencies = [] [project.urls] homepage = "https://github.com/beda-software/FHIRPathMappingLanguage/tree/main/python/" @@ -24,7 +20,7 @@ documentation = "https://github.com/beda-software/FHIRPathMappingLanguage/tree/m [tool.poetry] version = "0.1.0" -packages = [{include = "python", from = "src"}] +packages = [{ include = "fhirpath-mapping-language", from = "src" }] classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", @@ -38,8 +34,8 @@ classifiers = [ "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Software Development :: Libraries :: Python Modules", - ] +readme = ["README.md", "../README.md"] [tool.poetry.dependencies] python = ">=3.9,<4.0" diff --git a/python/src/fhirpath-mapping-language/.gitkeep b/python/src/fhirpath-mapping-language/.gitkeep new file mode 100644 index 0000000..e69de29 From c733c3932141614ae15a4e7899d1972c53b8f4ce Mon Sep 17 00:00:00 2001 From: Dmitry Shutov Date: Tue, 25 Feb 2025 12:13:04 +0700 Subject: [PATCH 05/23] Move tests data to the root --- python/pyproject.toml | 1 - .../src/utils => tests}/__data__/complex-example.aidbox.yaml | 0 .../src/utils => tests}/__data__/complex-example.fhir.yaml | 0 ts/server/src/utils/__data__ | 1 + 4 files changed, 1 insertion(+), 1 deletion(-) rename {ts/server/src/utils => tests}/__data__/complex-example.aidbox.yaml (100%) rename {ts/server/src/utils => tests}/__data__/complex-example.fhir.yaml (100%) create mode 120000 ts/server/src/utils/__data__ diff --git a/python/pyproject.toml b/python/pyproject.toml index 4e9196f..bb5004e 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -40,7 +40,6 @@ readme = ["README.md", "../README.md"] [tool.poetry.dependencies] python = ">=3.9,<4.0" - [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"] build-backend = "poetry.core.masonry.api" diff --git a/ts/server/src/utils/__data__/complex-example.aidbox.yaml b/tests/__data__/complex-example.aidbox.yaml similarity index 100% rename from ts/server/src/utils/__data__/complex-example.aidbox.yaml rename to tests/__data__/complex-example.aidbox.yaml diff --git a/ts/server/src/utils/__data__/complex-example.fhir.yaml b/tests/__data__/complex-example.fhir.yaml similarity index 100% rename from ts/server/src/utils/__data__/complex-example.fhir.yaml rename to tests/__data__/complex-example.fhir.yaml diff --git a/ts/server/src/utils/__data__ b/ts/server/src/utils/__data__ new file mode 120000 index 0000000..2a4509b --- /dev/null +++ b/ts/server/src/utils/__data__ @@ -0,0 +1 @@ +/Users/dmitrijsutov/projects/bedasoftware/open-source/FHIRPathMappingLanguage/tests/__data__ \ No newline at end of file From 5c2af85c10f6ed14785616b001040b87d141b408 Mon Sep 17 00:00:00 2001 From: Dmitry Shutov Date: Tue, 25 Feb 2025 14:25:00 +0700 Subject: [PATCH 06/23] Install ruff and pytest --- python/.gitignore | 2 +- python/pyproject.toml | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/python/.gitignore b/python/.gitignore index 6a4cfb4..d27abdc 100644 --- a/python/.gitignore +++ b/python/.gitignore @@ -1 +1 @@ -./poetry.lock \ No newline at end of file +poetry.lock \ No newline at end of file diff --git a/python/pyproject.toml b/python/pyproject.toml index bb5004e..c0cba40 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -6,11 +6,11 @@ maintainers = [ { name = "Vadim Laletin", email = "vadim@beda.software" }, { name = "Ilya Beda", email = "ilya@beda.software" }, ] -license = { text = "BSD-3-Clause" } +license = { text = "MIT" } requires-python = ">=3.9" keywords = ["fhir", "fhirpath"] dynamic = ["version", "classifiers"] -dependencies = [] +dependencies = ["fhirpathpy (>=1.2.1,<2.0.0)"] [project.urls] homepage = "https://github.com/beda-software/FHIRPathMappingLanguage/tree/main/python/" @@ -40,6 +40,16 @@ readme = ["README.md", "../README.md"] [tool.poetry.dependencies] python = ">=3.9,<4.0" +[tool.poetry.group.dev.dependencies] +ruff = "^0.9.7" + + +[tool.poetry.group.test.dependencies] +pytest = "^8.3.4" + [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"] build-backend = "poetry.core.masonry.api" + +[tool.ruff] +line-length = 100 From 57470efbeffce341911683dc8dd0dcc81796c46a Mon Sep 17 00:00:00 2001 From: Dmitry Shutov Date: Wed, 26 Feb 2025 10:01:35 +0700 Subject: [PATCH 07/23] WIP: prepare to test --- python/.gitignore | 4 +++- .../src/fhirpath-mapping-language/__init__.py | 1 + .../utils/__init__.py | 1 + .../utils/extract.py | 20 +++++++++++++++++++ .../fhirpath-mapping-language/utils/types.py | 15 ++++++++++++++ .../.gitkeep => tests/utils/test_extract.py} | 0 6 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 python/src/fhirpath-mapping-language/__init__.py create mode 100644 python/src/fhirpath-mapping-language/utils/__init__.py create mode 100644 python/src/fhirpath-mapping-language/utils/extract.py create mode 100644 python/src/fhirpath-mapping-language/utils/types.py rename python/{src/fhirpath-mapping-language/.gitkeep => tests/utils/test_extract.py} (100%) diff --git a/python/.gitignore b/python/.gitignore index d27abdc..016f114 100644 --- a/python/.gitignore +++ b/python/.gitignore @@ -1 +1,3 @@ -poetry.lock \ No newline at end of file +poetry.lock +dev.code-workspace +.mypy_cache diff --git a/python/src/fhirpath-mapping-language/__init__.py b/python/src/fhirpath-mapping-language/__init__.py new file mode 100644 index 0000000..ae9129c --- /dev/null +++ b/python/src/fhirpath-mapping-language/__init__.py @@ -0,0 +1 @@ +from . import utils as utils diff --git a/python/src/fhirpath-mapping-language/utils/__init__.py b/python/src/fhirpath-mapping-language/utils/__init__.py new file mode 100644 index 0000000..8824e81 --- /dev/null +++ b/python/src/fhirpath-mapping-language/utils/__init__.py @@ -0,0 +1 @@ +from . import extract as extract \ No newline at end of file diff --git a/python/src/fhirpath-mapping-language/utils/extract.py b/python/src/fhirpath-mapping-language/utils/extract.py new file mode 100644 index 0000000..4d5efaa --- /dev/null +++ b/python/src/fhirpath-mapping-language/utils/extract.py @@ -0,0 +1,20 @@ +from typing import Any +from .types import Context, Model, Resource + + +class FPMLValidationError(Exception): + def __init__( + self, + message: str, + path: str, + ) -> None: + self.path = path + self.message = message + + super().__init__(f"{self.message}. Path: {self.path}") + + +def resolve_template( + resource: Resource, template: Any, context: Context = None, model: Model | None = None +): + pass diff --git a/python/src/fhirpath-mapping-language/utils/types.py b/python/src/fhirpath-mapping-language/utils/types.py new file mode 100644 index 0000000..614db53 --- /dev/null +++ b/python/src/fhirpath-mapping-language/utils/types.py @@ -0,0 +1,15 @@ +from typing import Any, TypedDict + + +type Resource = dict[str, Any] +type Context = dict[str, Any] | None + + +class Model(TypedDict): + choiceTypePaths: dict[str, list[str]] + pathsDefinedElsewhere: dict[str, str] + type2Parent: dict[str, str] + path2Type: dict[str, str] + +class UserInvocationTable: + pass diff --git a/python/src/fhirpath-mapping-language/.gitkeep b/python/tests/utils/test_extract.py similarity index 100% rename from python/src/fhirpath-mapping-language/.gitkeep rename to python/tests/utils/test_extract.py From bd8f497859dc9ac74a1209052469221c17e37625 Mon Sep 17 00:00:00 2001 From: Vadim Laletin Date: Wed, 26 Feb 2025 11:20:17 +0100 Subject: [PATCH 08/23] Fix path to test data --- ts/server/src/utils/__data__ | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts/server/src/utils/__data__ b/ts/server/src/utils/__data__ index 2a4509b..63d46a1 120000 --- a/ts/server/src/utils/__data__ +++ b/ts/server/src/utils/__data__ @@ -1 +1 @@ -/Users/dmitrijsutov/projects/bedasoftware/open-source/FHIRPathMappingLanguage/tests/__data__ \ No newline at end of file +../../../../tests/__data__ \ No newline at end of file From cd420a74e636393615a8a5148d2b76ab54c45e5c Mon Sep 17 00:00:00 2001 From: Vadim Laletin Date: Fri, 28 Feb 2025 00:46:20 +0100 Subject: [PATCH 09/23] Add mypy --- python/.gitignore | 1 + python/README.md | 14 +++++++++++++- python/pyproject.toml | 13 ++++++++++--- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/python/.gitignore b/python/.gitignore index 016f114..a997604 100644 --- a/python/.gitignore +++ b/python/.gitignore @@ -1,3 +1,4 @@ poetry.lock dev.code-workspace .mypy_cache +__pycache__ diff --git a/python/README.md b/python/README.md index 1072d05..b1ce1e3 100644 --- a/python/README.md +++ b/python/README.md @@ -1 +1,13 @@ -# FHIRPathMappingLanguage \ No newline at end of file +# FHIRPathMappingLanguage - fpml python package + +## Installation + +```bash +pip install fpml +``` + +## Usage + +```python +from fpml import resolve_template +``` diff --git a/python/pyproject.toml b/python/pyproject.toml index c0cba40..19b32fa 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "fhirpath-mapping-language" +name = "fpml" description = "The FHIRPath mapping language is a data DSL designed to convert data from QuestionnaireResponse (and not only) to any FHIR Resource." authors = [{ name = "Beda Software", email = "ilya@beda.software" }] maintainers = [ @@ -20,7 +20,7 @@ documentation = "https://github.com/beda-software/FHIRPathMappingLanguage/tree/m [tool.poetry] version = "0.1.0" -packages = [{ include = "fhirpath-mapping-language", from = "src" }] +packages = [{ include = "fpml" }] classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", @@ -42,7 +42,7 @@ python = ">=3.9,<4.0" [tool.poetry.group.dev.dependencies] ruff = "^0.9.7" - +mypy = "^1.15.0" [tool.poetry.group.test.dependencies] pytest = "^8.3.4" @@ -52,4 +52,11 @@ requires = ["poetry-core>=2.0.0,<3.0.0"] build-backend = "poetry.core.masonry.api" [tool.ruff] +files = ["fpml", "tests"] +target-version = "py39" line-length = 100 + +[tool.mypy] +files = ["fpml", "tests"] +ignore_missing_imports = true +check_untyped_defs = true \ No newline at end of file From 31dbd54f1c9d7f5166165e66f742f50e2eafef51 Mon Sep 17 00:00:00 2001 From: Vadim Laletin Date: Fri, 28 Feb 2025 00:48:14 +0100 Subject: [PATCH 10/23] Port typescript extract tests into pytest --- python/fpml/__init__.py | 5 + .../test_extract.py => fpml/core/__init__.py} | 0 python/fpml/core/constants.py | 5 + python/fpml/core/exceptions.py | 12 + python/fpml/core/extract.py | 23 + python/fpml/core/types.py | 40 + python/fpml/core/utils.py | 17 + .../src/fhirpath-mapping-language/__init__.py | 1 - .../utils/__init__.py | 1 - .../utils/extract.py | 20 - .../fhirpath-mapping-language/utils/types.py | 15 - python/tests/__init__.py | 0 python/tests/conftest.py | 0 python/tests/core/__init__.py | 0 python/tests/core/test_extract.py | 697 ++++++++++++++++++ 15 files changed, 799 insertions(+), 37 deletions(-) create mode 100644 python/fpml/__init__.py rename python/{tests/utils/test_extract.py => fpml/core/__init__.py} (100%) create mode 100644 python/fpml/core/constants.py create mode 100644 python/fpml/core/exceptions.py create mode 100644 python/fpml/core/extract.py create mode 100644 python/fpml/core/types.py create mode 100644 python/fpml/core/utils.py delete mode 100644 python/src/fhirpath-mapping-language/__init__.py delete mode 100644 python/src/fhirpath-mapping-language/utils/__init__.py delete mode 100644 python/src/fhirpath-mapping-language/utils/extract.py delete mode 100644 python/src/fhirpath-mapping-language/utils/types.py create mode 100644 python/tests/__init__.py create mode 100644 python/tests/conftest.py create mode 100644 python/tests/core/__init__.py create mode 100644 python/tests/core/test_extract.py diff --git a/python/fpml/__init__.py b/python/fpml/__init__.py new file mode 100644 index 0000000..4ce02bb --- /dev/null +++ b/python/fpml/__init__.py @@ -0,0 +1,5 @@ +from .core.extract import resolve_template +from .core.exceptions import FPMLValidationError + + +__all__ = ["resolve_template", "FPMLValidationError"] diff --git a/python/tests/utils/test_extract.py b/python/fpml/core/__init__.py similarity index 100% rename from python/tests/utils/test_extract.py rename to python/fpml/core/__init__.py diff --git a/python/fpml/core/constants.py b/python/fpml/core/constants.py new file mode 100644 index 0000000..7760748 --- /dev/null +++ b/python/fpml/core/constants.py @@ -0,0 +1,5 @@ +# special root node key for the simplification +root_node_key = "__rootNode__" + +# undefined is a special object used to remove keys from the object (similar to JS) +undefined = object() diff --git a/python/fpml/core/exceptions.py b/python/fpml/core/exceptions.py new file mode 100644 index 0000000..56838be --- /dev/null +++ b/python/fpml/core/exceptions.py @@ -0,0 +1,12 @@ + +from .types import Path +from .constants import root_node_key + +class FPMLValidationError(Exception): + def __init__(self, message: str, path: Path) -> None: + path_str = ".".join(str(x) for x in path if x != root_node_key) + super().__init__(f"{message}. Path '{path_str}'") + + self.error_message = message + self.error_path = path_str + diff --git a/python/fpml/core/extract.py b/python/fpml/core/extract.py new file mode 100644 index 0000000..09b8f0f --- /dev/null +++ b/python/fpml/core/extract.py @@ -0,0 +1,23 @@ +from typing import Any, Optional + + +from .types import ( + Context, + FPOptions, + Model, + Resource, +) + + +def resolve_template( + resource: Resource, + template: Any, + context: Optional[Context] = None, + model: Optional[Model] = None, + fp_options: Optional[FPOptions] = None, + strict: bool = False, +) -> Any: + assert fp_options is None, "fp_options are not supported" + assert strict is False, "strict is not supported yet" + + return {} \ No newline at end of file diff --git a/python/fpml/core/types.py b/python/fpml/core/types.py new file mode 100644 index 0000000..61fe7c9 --- /dev/null +++ b/python/fpml/core/types.py @@ -0,0 +1,40 @@ +from typing import Any, Callable, Optional, TypedDict, Union + + +Resource = dict[str, Any] +Node = Any +DictNode = dict[str, Any] +StrNode = str +Context = dict[str, Any] + +Path = list[Union[str, int]] + + +class Model(TypedDict): + choiceTypePaths: dict[str, list[str]] + pathsDefinedElsewhere: dict[str, str] + type2Parent: dict[str, str] + path2Type: dict[str, str] + + +class FPOptions(TypedDict): + pass + + +class MatcherResult(TypedDict): + node: Optional[Node] + + +Matcher = Callable[ + [ + Path, + Resource, + DictNode, + Context, + Optional[Model], + Optional[FPOptions], + ], + Optional[MatcherResult], +] + +Transformer = Callable[[Path, Node, Context], tuple[Node, Context]] diff --git a/python/fpml/core/utils.py b/python/fpml/core/utils.py new file mode 100644 index 0000000..d73d6b6 --- /dev/null +++ b/python/fpml/core/utils.py @@ -0,0 +1,17 @@ +from typing import Any, Optional + + +def flatten(lst: list): + result = [] + for item in lst: + if isinstance(item, list): + result.extend(flatten(item)) + else: + result.append(item) + return result + + +def omit_key(obj: dict[str, Any], key: Optional[str]) -> dict[str, Any]: + if key is None: + return obj + return {k: v for k, v in obj.items() if k != key} diff --git a/python/src/fhirpath-mapping-language/__init__.py b/python/src/fhirpath-mapping-language/__init__.py deleted file mode 100644 index ae9129c..0000000 --- a/python/src/fhirpath-mapping-language/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import utils as utils diff --git a/python/src/fhirpath-mapping-language/utils/__init__.py b/python/src/fhirpath-mapping-language/utils/__init__.py deleted file mode 100644 index 8824e81..0000000 --- a/python/src/fhirpath-mapping-language/utils/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import extract as extract \ No newline at end of file diff --git a/python/src/fhirpath-mapping-language/utils/extract.py b/python/src/fhirpath-mapping-language/utils/extract.py deleted file mode 100644 index 4d5efaa..0000000 --- a/python/src/fhirpath-mapping-language/utils/extract.py +++ /dev/null @@ -1,20 +0,0 @@ -from typing import Any -from .types import Context, Model, Resource - - -class FPMLValidationError(Exception): - def __init__( - self, - message: str, - path: str, - ) -> None: - self.path = path - self.message = message - - super().__init__(f"{self.message}. Path: {self.path}") - - -def resolve_template( - resource: Resource, template: Any, context: Context = None, model: Model | None = None -): - pass diff --git a/python/src/fhirpath-mapping-language/utils/types.py b/python/src/fhirpath-mapping-language/utils/types.py deleted file mode 100644 index 614db53..0000000 --- a/python/src/fhirpath-mapping-language/utils/types.py +++ /dev/null @@ -1,15 +0,0 @@ -from typing import Any, TypedDict - - -type Resource = dict[str, Any] -type Context = dict[str, Any] | None - - -class Model(TypedDict): - choiceTypePaths: dict[str, list[str]] - pathsDefinedElsewhere: dict[str, str] - type2Parent: dict[str, str] - path2Type: dict[str, str] - -class UserInvocationTable: - pass diff --git a/python/tests/__init__.py b/python/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/tests/conftest.py b/python/tests/conftest.py new file mode 100644 index 0000000..e69de29 diff --git a/python/tests/core/__init__.py b/python/tests/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/tests/core/test_extract.py b/python/tests/core/test_extract.py new file mode 100644 index 0000000..f05b2c0 --- /dev/null +++ b/python/tests/core/test_extract.py @@ -0,0 +1,697 @@ +import pytest + +from fpml.core.extract import FPMLValidationError, resolve_template +from fpml.core.constants import undefined + + +@pytest.mark.skip("Strict mode is not implemented") +def test_transformation_fails_on_access_props_of_resource_in_strict_mode(): + resource = {"list": [{"key": 1}, {"key": 2}, {"key": 3}]} + with pytest.raises(FPMLValidationError): + resolve_template(resource, {"key": "{{ list }}"}, {}, None, None, True) + + +def test_transformation_for_empty_object_return_empty_object(): + assert resolve_template({}, {}) == {} + + +def test_transformation_for_empty_array_return_empty_array(): + assert resolve_template({}, []) == [] + + +def test_transformation_for_array_of_arrays_returns_flattened_array(): + assert resolve_template({}, [[1, 2, 3], [4, 5, 6]]) == [1, 2, 3, 4, 5, 6] + + +def test_transformation_for_array_with_null_values_returns_compacted_array(): + assert resolve_template({}, [[1, None, 2, None, 3]]) == [1, 2, 3] + + +def test_transformation_for_array_with_undefined_values_returns_compacted_array(): + assert resolve_template({}, [[1, undefined, 2, undefined, 3]]) == [1, 2, 3] + + +def test_transformation_for_object_with_null_keys_returns_null_keys(): + assert resolve_template({}, {"key": None}) == {"key": None} + + +def test_transformation_for_object_with_undefined_keys_clears_undefined_keys(): + assert resolve_template({}, {"key": undefined}) == {} + + +def test_transformation_for_object_with_non_null_keys_returns_non_null_keys(): + assert resolve_template({}, {"key": 1}) == {"key": 1} + + +def test_transformation_for_array_of_objects_returns_original_array(): + assert resolve_template({}, [{"list": [1, 2, 3]}, {"list": [4, 5, 6]}]) == [ + {"list": [1, 2, 3]}, + {"list": [4, 5, 6]}, + ] + + +def test_transformation_for_null_returns_null(): + assert resolve_template({}, None) is None + + +def test_transformation_for_constant_string_returns_constant_string(): + assert resolve_template({}, "string") == "string" + + +def test_transformation_for_constant_number_returns_constant_number(): + assert resolve_template({}, 1) == 1 + + +def test_transformation_for_false_returns_false(): + assert resolve_template({}, False) is False + + +def test_transformation_for_true_returns_true(): + assert resolve_template({}, True) is True + + +def test_transformation_for_non_empty_array_expression_return_first_element(): + resource = {"list": [{"key": 1}, {"key": 2}, {"key": 3}]} + assert resolve_template(resource, "{{ list }}") == {"key": 1} + + +def test_transformation_for_empty_array_expression_clears_undefined_keys(): + resource = {"list": []} + assert resolve_template(resource, {"result": "{{ list.where($this = 0) }}"}) == {} + + +def test_transformation_for_empty_array_nullable_expression_returns_null(): + resource = {"list": []} + assert resolve_template(resource, {"result": "{{+ list.where($this = 0) +}}"}) == { + "result": None + } + + +def test_transformation_for_template_expression_returns_resolved_template(): + resource = {"list": [{"key": 1}, {"key": 2}, {"key": 3}]} + assert ( + resolve_template( + resource, "/{{ list[0].key }}/{{ list[1].key }}/{{ list[2].key }}" + ) + == "/1/2/3" + ) + + +def test_transformation_for_empty_array_template_expression_clears_undefined_keys(): + resource = {"list": [{"key": 1}, {"key": 2}, {"key": 3}]} + assert ( + resolve_template( + resource, + { + "result": "/Patient/{{ list.where($this = 0) }}/_history/{{ list.last() }}" + }, + ) + == {} + ) + + +def test_transformation_for_empty_array_nullable_template_expression_returns_null(): + resource = {"list": [{"key": 1}, {"key": 2}, {"key": 3}]} + assert resolve_template( + resource, + {"result": "/Patient/{{+ list.where($this = 0) +}}/_history/{{ list.last() }}"}, + ) == {"result": None} + + +def test_transformation_for_multiline_template_works_properly(): + resource = {"list": [{"key": 1}]} + assert resolve_template(resource, "{{\nlist.where(\n$this.key=1\n).key\n}}") == 1 + + +def test_transformation_fails_with_incorrect_fhirpath_expression(): + with pytest.raises(FPMLValidationError): + resolve_template({}, "{{ item.where(linkId='a) }}") + + +def test_context_block_resolve_template() -> None: + resource = { + "foo": "bar", + "list": [{"key": "a"}, {"key": "b"}, {"key": "c"}], + } + template = { + "list": { + "{{ list }}": { + "key": "{{ key }}", + "foo": "{{ %root.foo }}", + }, + }, + } + + context = {"root": resource} + + expected_result = { + "list": [ + {"key": "a", "foo": "bar"}, + {"key": "b", "foo": "bar"}, + {"key": "c", "foo": "bar"}, + ] + } + + result = resolve_template(resource, template, context) + + assert result == expected_result + + +def test_assign_block_single_var_as_object() -> None: + resource = { + "resourceType": "Resource", + "sourceValue": 100, + } + assert resolve_template( + resource, + { + "{% assign %}": {"var": 100}, + "value": "{{ %var }}", + }, + ) == {"value": 100} + + +def test_assign_block_multiple_vars_as_array_of_objects() -> None: + resource = { + "resourceType": "Resource", + "sourceValue": 100, + } + assert resolve_template( + resource, + { + "{% assign %}": [{"varA": 100}, {"varB": "{{ %varA + 100}}"}], + "valueA": "{{ %varA }}", + "valueB": "{{ %varB }}", + }, + ) == { + "valueA": 100, + "valueB": 200, + } + + +def test_assign_block_isolated_nested_context() -> None: + resource = { + "resourceType": "Resource", + "sourceValue": 100, + } + assert resolve_template( + resource, + { + "{% assign %}": {"varC": 100}, + "nested": { + "{% assign %}": {"varC": 200}, + "valueC": "{{ %varC }}", + }, + "valueC": "{{ %varC }}", + }, + ) == { + "valueC": 100, + "nested": {"valueC": 200}, + } + + +def test_assign_block_full_example() -> None: + resource = { + "resourceType": "Resource", + "sourceValue": 100, + } + assert resolve_template( + resource, + { + "{% assign %}": [ + { + "varA": { + "{% assign %}": [ + {"varX": "{{ Resource.sourceValue.first() }}"} + ], + "x": "{{ %varX }}", + } + }, + {"varB": "{{ %varA.x + 1 }}"}, + {"varC": 0}, + ], + "nested": { + "{% assign %}": {"varC": "{{ %varA.x + %varB }}"}, + "valueA": "{{ %varA }}", + "valueB": "{{ %varB }}", + "valueC": "{{ %varC }}", + }, + "valueA": "{{ %varA }}", + "valueB": "{{ %varB }}", + "valueC": "{{ %varC }}", + }, + ) == { + "valueA": {"x": 100}, + "valueB": 101, + "valueC": 0, + "nested": { + "valueA": {"x": 100}, + "valueB": 101, + "valueC": 201, + }, + } + + +def test_assign_block_fails_with_multiple_keys_in_object() -> None: + resource = { + "resourceType": "Resource", + "sourceValue": 100, + } + with pytest.raises(FPMLValidationError): + resolve_template( + resource, + { + "{% assign %}": {"varA": 100, "varB": 200}, + "value": "{{ %var }}", + }, + ) + + +def test_assign_block_fails_with_multiple_keys_in_array_of_objects() -> None: + resource = { + "resourceType": "Resource", + "sourceValue": 100, + } + with pytest.raises(FPMLValidationError): + resolve_template( + resource, + { + "{% assign %}": [{"varA": 100, "varB": 200}], + "value": "{{ %var }}", + }, + ) + + +def test_assign_block_fails_with_non_array_and_non_object_as_value() -> None: + resource = { + "resourceType": "Resource", + "sourceValue": 100, + } + with pytest.raises(FPMLValidationError): + resolve_template( + resource, + { + "{% assign %}": 1, + "value": "{{ %var }}", + }, + ) + + +def test_for_block_resolve_template_full_example(): + context = { + "foo": "bar", + "list": [{"key": "a"}, {"key": "b"}, {"key": "c"}], + } + template = { + "listArr": [ + { + "{% for index, item in list %}": { + "key": "{{ %item.key }}", + "foo": "{{ foo }}", + "index": "{{ %index }}", + }, + }, + { + "{% for item in list %}": { + "key": "{{ %item.key }}", + "foo": "{{ foo }}", + }, + }, + ], + "listObj": { + "{% for item in list %}": { + "key": "{{ %item.key }}", + "foo": "{{ foo }}", + }, + }, + } + expected_result = { + "listArr": [ + {"key": "a", "foo": "bar", "index": 0}, + {"key": "b", "foo": "bar", "index": 1}, + {"key": "c", "foo": "bar", "index": 2}, + {"key": "a", "foo": "bar"}, + {"key": "b", "foo": "bar"}, + {"key": "c", "foo": "bar"}, + ], + "listObj": [ + {"key": "a", "foo": "bar"}, + {"key": "b", "foo": "bar"}, + {"key": "c", "foo": "bar"}, + ], + } + assert resolve_template(context, template) == expected_result + + +def test_for_block_resolve_template_local_assign(): + template = { + "{% assign %}": { + "localList": [{"key": "a"}, {"key": "b"}, {"key": "c"}], + }, + "listArr": [ + { + "{% for item in %localList %}": { + "key": "{{ %item.key }}", + }, + }, + ], + } + expected_result = {"listArr": [{"key": "a"}, {"key": "b"}, {"key": "c"}]} + assert resolve_template({}, template) == expected_result + + +def test_for_block_resolve_template_fails_with_other_keys(): + context = {"list": [1, 2, 3]} + template = { + "userKey": 1, + "{% for key in %list %}": "{{ %key }}", + } + with pytest.raises(FPMLValidationError): + resolve_template(context, template) + + +def test_if_block_returns_if_branch_for_truthy_condition_at_root_level(): + resource = { + "key": "value", + } + result = resolve_template( + resource, + { + "{% if key = 'value' %}": {"nested": "{{ 'true' + key }}"}, + "{% else %}": {"nested": "{{ 'false' + key }}"}, + }, + ) + assert result == {"nested": "truevalue"} + + +def test_if_block_returns_if_branch_for_truthy_condition(): + resource = { + "key": "value", + } + result = resolve_template( + resource, + { + "result": { + "{% if key = 'value' %}": {"nested": "{{ 'true' + key }}"}, + "{% else %}": {"nested": "{{ 'false' + key }}"}, + }, + }, + ) + assert result == {"result": {"nested": "truevalue"}} + + +def test_if_block_returns_if_branch_for_truthy_condition_without_else_branch(): + resource = { + "key": "value", + } + result = resolve_template( + resource, + { + "result": { + "{% if key = 'value' %}": {"nested": "{{ 'true' + key }}"}, + }, + }, + ) + assert result == {"result": {"nested": "truevalue"}} + + +def test_if_block_returns_else_branch_for_falsy_condition(): + resource = { + "key": "value", + } + result = resolve_template( + resource, + { + "result": { + "{% if key != 'value' %}": {"nested": "{{ 'true' + key }}"}, + "{% else %}": {"nested": "{{ 'false' + key }}"}, + }, + }, + ) + assert result == {"result": {"nested": "falsevalue"}} + + +def test_if_block_clears_undefined_keys_for_falsy_condition_without_else_branch(): + resource = { + "key": "value", + } + result = resolve_template( + resource, + { + "result": { + "{% if key != 'value' %}": {"nested": "{{ 'true' + key }}"}, + }, + }, + ) + assert result == {} + + +def test_if_block_returns_null_for_falsy_condition_with_nullable_else_branch(): + resource = { + "key": "value", + } + result = resolve_template( + resource, + { + "result": { + "{% if key != 'value' %}": {"nested": "{{ 'true' + key }}"}, + "{% else %}": "{{+ {} +}}", + }, + }, + ) + assert result == {"result": None} + + +def test_if_block_returns_if_branch_for_nested_if(): + resource = { + "key": "value", + } + result = resolve_template( + resource, + { + "result": { + "{% if key = 'value' %}": { + "{% if key = 'value' %}": "value", + }, + }, + }, + ) + assert result == {"result": "value"} + + +def test_if_block_returns_else_branch_for_nested_else(): + resource = { + "key": "value", + } + result = resolve_template( + resource, + { + "result": { + "{% if key != 'value' %}": None, + "{% else %}": { + "{% if key != 'value' %}": None, + "{% else %}": "value", + }, + }, + }, + ) + assert result == {"result": "value"} + + +def test_if_block_implicitly_merges_with_null_returned(): + resource = { + "key": "value", + } + result = resolve_template( + resource, + { + "result": { + "myKey": 1, + "{% if key = 'value' %}": None, + }, + }, + ) + assert result == {"result": {"myKey": 1}} + + +def test_if_block_implicitly_merges_with_object_returned_for_truthy_condition(): + resource = { + "key": "value", + } + result = resolve_template( + resource, + { + "result": { + "myKey": 1, + "{% if key = 'value' %}": {"anotherKey": 2}, + }, + }, + ) + assert result == {"result": {"myKey": 1, "anotherKey": 2}} + + +def test_if_block_implicitly_merges_with_object_returned_for_falsy_condition(): + resource = { + "key": "value", + } + result = resolve_template( + resource, + { + "result": { + "myKey": 1, + "{% if key != 'value' %}": {"anotherKey": 2}, + "{% else %}": {"anotherKey": 3}, + }, + }, + ) + assert result == {"result": {"myKey": 1, "anotherKey": 3}} + + +def test_if_block_fails_on_implicit_merge_with_non_object_returned_from_if_branch(): + resource = { + "key": "value", + } + with pytest.raises(FPMLValidationError): + resolve_template( + resource, + { + "result": { + "myKey": 1, + "{% if key = 'value' %}": [], + }, + }, + ) + + +def test_if_block_fails_on_implicit_merge_with_non_object_returned_from_else_branch(): + resource = { + "key": "value", + } + with pytest.raises(FPMLValidationError): + resolve_template( + resource, + { + "result": { + "myKey": 1, + "{% if key != 'value' %}": {}, + "{% else %}": [], + }, + }, + ) + + +def test_if_block_fails_on_multiple_if_blocks(): + resource = { + "key": "value", + } + with pytest.raises(FPMLValidationError): + resolve_template( + resource, + { + "result": { + "myKey": 1, + "{% if key != 'value' %}": {}, + "{% if key = 'value' %}": {}, + }, + }, + ) + + +def test_if_block_fails_on_multiple_else_blocks(): + resource = { + "key": "value", + } + with pytest.raises(FPMLValidationError): + resolve_template( + resource, + { + "result": { + "myKey": 1, + "{% if key != 'value' %}": {}, + "{% else %}": {}, + "{% else %}": {}, + }, + }, + ) + + +def test_if_block_fails_on_else_block_without_if_block(): + resource = { + "key": "value", + } + with pytest.raises(FPMLValidationError): + resolve_template( + resource, + { + "result": { + "myKey": 1, + "{% else %}": {}, + }, + }, + ) + + +def test_merge_block_implicitly_merges_into_the_node(): + resource = { + "key": "value", + } + result = resolve_template( + resource, + { + "b": 1, + "{% merge %}": {"a": 1}, + }, + ) + assert result == {"b": 1, "a": 1} + + +def test_merge_block_merges_multiple_blocks_within_order(): + resource = { + "key": "value", + } + result = resolve_template( + resource, + { + "{% merge %}": [{"a": 1}, {"b": 2}, {"a": 3}], + }, + ) + assert result == {"a": 3, "b": 2} + + +def test_merge_block_merges_multiple_blocks_containing_nulls(): + resource = { + "key": "value", + } + result = resolve_template( + resource, + { + "{% merge %}": [{"a": 1}, None, {"b": 2}], + }, + ) + assert result == {"a": 1, "b": 2} + + +def test_merge_block_merges_multiple_blocks_containing_undefined(): + resource = { + "key": "value", + } + result = resolve_template( + resource, + { + "{% merge %}": [{"a": 1}, undefined, {"b": 2}], + }, + ) + assert result == {"a": 1, "b": 2} + + +def test_merge_block_fails_on_merge_with_non_object(): + resource = { + "key": "value", + } + with pytest.raises(FPMLValidationError): + resolve_template( + resource, + { + "{% merge %}": [1, 2], + }, + ) From beddd19cc9181a10ac51a2b4dba29d9738dad099 Mon Sep 17 00:00:00 2001 From: Vadim Laletin Date: Fri, 28 Feb 2025 00:56:53 +0100 Subject: [PATCH 11/23] Format, lint and typecheck --- python/fpml/__init__.py | 5 +- python/fpml/core/exceptions.py | 5 +- python/fpml/core/types.py | 1 - python/pyproject.toml | 5 +- python/tests/core/test_extract.py | 183 +++++++++++++++--------------- 5 files changed, 97 insertions(+), 102 deletions(-) diff --git a/python/fpml/__init__.py b/python/fpml/__init__.py index 4ce02bb..c429df3 100644 --- a/python/fpml/__init__.py +++ b/python/fpml/__init__.py @@ -1,5 +1,4 @@ -from .core.extract import resolve_template from .core.exceptions import FPMLValidationError +from .core.extract import resolve_template - -__all__ = ["resolve_template", "FPMLValidationError"] +__all__ = ["FPMLValidationError", "resolve_template"] diff --git a/python/fpml/core/exceptions.py b/python/fpml/core/exceptions.py index 56838be..1034234 100644 --- a/python/fpml/core/exceptions.py +++ b/python/fpml/core/exceptions.py @@ -1,6 +1,6 @@ - -from .types import Path from .constants import root_node_key +from .types import Path + class FPMLValidationError(Exception): def __init__(self, message: str, path: Path) -> None: @@ -9,4 +9,3 @@ def __init__(self, message: str, path: Path) -> None: self.error_message = message self.error_path = path_str - diff --git a/python/fpml/core/types.py b/python/fpml/core/types.py index 61fe7c9..e58c2bf 100644 --- a/python/fpml/core/types.py +++ b/python/fpml/core/types.py @@ -1,6 +1,5 @@ from typing import Any, Callable, Optional, TypedDict, Union - Resource = dict[str, Any] Node = Any DictNode = dict[str, Any] diff --git a/python/pyproject.toml b/python/pyproject.toml index 19b32fa..f516bf5 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -52,10 +52,13 @@ requires = ["poetry-core>=2.0.0,<3.0.0"] build-backend = "poetry.core.masonry.api" [tool.ruff] -files = ["fpml", "tests"] target-version = "py39" line-length = 100 +[tool.ruff.lint] +select = ["I", "E", "F", "N", "B", "C4", "PT", "UP", "I001", "A", "RET", "TID251", "RUF", "SIM", "PYI", "T20", "PIE", "G", "ISC", "PL"] + + [tool.mypy] files = ["fpml", "tests"] ignore_missing_imports = true diff --git a/python/tests/core/test_extract.py b/python/tests/core/test_extract.py index f05b2c0..7094d22 100644 --- a/python/tests/core/test_extract.py +++ b/python/tests/core/test_extract.py @@ -1,135 +1,132 @@ import pytest -from fpml.core.extract import FPMLValidationError, resolve_template from fpml.core.constants import undefined +from fpml.core.extract import FPMLValidationError, resolve_template +from fpml.core.types import Resource @pytest.mark.skip("Strict mode is not implemented") -def test_transformation_fails_on_access_props_of_resource_in_strict_mode(): - resource = {"list": [{"key": 1}, {"key": 2}, {"key": 3}]} +def test_transformation_fails_on_access_props_of_resource_in_strict_mode() -> None: + resource: Resource = {"list": [{"key": 1}, {"key": 2}, {"key": 3}]} with pytest.raises(FPMLValidationError): resolve_template(resource, {"key": "{{ list }}"}, {}, None, None, True) -def test_transformation_for_empty_object_return_empty_object(): +def test_transformation_for_empty_object_return_empty_object() -> None: assert resolve_template({}, {}) == {} -def test_transformation_for_empty_array_return_empty_array(): +def test_transformation_for_empty_array_return_empty_array() -> None: assert resolve_template({}, []) == [] -def test_transformation_for_array_of_arrays_returns_flattened_array(): +def test_transformation_for_array_of_arrays_returns_flattened_array() -> None: assert resolve_template({}, [[1, 2, 3], [4, 5, 6]]) == [1, 2, 3, 4, 5, 6] -def test_transformation_for_array_with_null_values_returns_compacted_array(): +def test_transformation_for_array_with_null_values_returns_compacted_array() -> None: assert resolve_template({}, [[1, None, 2, None, 3]]) == [1, 2, 3] -def test_transformation_for_array_with_undefined_values_returns_compacted_array(): +def test_transformation_for_array_with_undefined_values_returns_compacted_array() -> None: assert resolve_template({}, [[1, undefined, 2, undefined, 3]]) == [1, 2, 3] -def test_transformation_for_object_with_null_keys_returns_null_keys(): +def test_transformation_for_object_with_null_keys_returns_null_keys() -> None: assert resolve_template({}, {"key": None}) == {"key": None} -def test_transformation_for_object_with_undefined_keys_clears_undefined_keys(): +def test_transformation_for_object_with_undefined_keys_clears_undefined_keys() -> None: assert resolve_template({}, {"key": undefined}) == {} -def test_transformation_for_object_with_non_null_keys_returns_non_null_keys(): +def test_transformation_for_object_with_non_null_keys_returns_non_null_keys() -> None: assert resolve_template({}, {"key": 1}) == {"key": 1} -def test_transformation_for_array_of_objects_returns_original_array(): +def test_transformation_for_array_of_objects_returns_original_array() -> None: assert resolve_template({}, [{"list": [1, 2, 3]}, {"list": [4, 5, 6]}]) == [ {"list": [1, 2, 3]}, {"list": [4, 5, 6]}, ] -def test_transformation_for_null_returns_null(): +def test_transformation_for_null_returns_null() -> None: assert resolve_template({}, None) is None -def test_transformation_for_constant_string_returns_constant_string(): +def test_transformation_for_constant_string_returns_constant_string() -> None: assert resolve_template({}, "string") == "string" -def test_transformation_for_constant_number_returns_constant_number(): +def test_transformation_for_constant_number_returns_constant_number() -> None: assert resolve_template({}, 1) == 1 -def test_transformation_for_false_returns_false(): +def test_transformation_for_false_returns_false() -> None: assert resolve_template({}, False) is False -def test_transformation_for_true_returns_true(): +def test_transformation_for_true_returns_true() -> None: assert resolve_template({}, True) is True -def test_transformation_for_non_empty_array_expression_return_first_element(): - resource = {"list": [{"key": 1}, {"key": 2}, {"key": 3}]} +def test_transformation_for_non_empty_array_expression_return_first_element() -> None: + resource: Resource = {"list": [{"key": 1}, {"key": 2}, {"key": 3}]} assert resolve_template(resource, "{{ list }}") == {"key": 1} -def test_transformation_for_empty_array_expression_clears_undefined_keys(): - resource = {"list": []} +def test_transformation_for_empty_array_expression_clears_undefined_keys() -> None: + resource: Resource = {"list": []} assert resolve_template(resource, {"result": "{{ list.where($this = 0) }}"}) == {} -def test_transformation_for_empty_array_nullable_expression_returns_null(): - resource = {"list": []} +def test_transformation_for_empty_array_nullable_expression_returns_null() -> None: + resource: Resource = {"list": []} assert resolve_template(resource, {"result": "{{+ list.where($this = 0) +}}"}) == { "result": None } -def test_transformation_for_template_expression_returns_resolved_template(): - resource = {"list": [{"key": 1}, {"key": 2}, {"key": 3}]} +def test_transformation_for_template_expression_returns_resolved_template() -> None: + resource: Resource = {"list": [{"key": 1}, {"key": 2}, {"key": 3}]} assert ( - resolve_template( - resource, "/{{ list[0].key }}/{{ list[1].key }}/{{ list[2].key }}" - ) + resolve_template(resource, "/{{ list[0].key }}/{{ list[1].key }}/{{ list[2].key }}") == "/1/2/3" ) -def test_transformation_for_empty_array_template_expression_clears_undefined_keys(): - resource = {"list": [{"key": 1}, {"key": 2}, {"key": 3}]} +def test_transformation_for_empty_array_template_expression_clears_undefined_keys() -> None: + resource: Resource = {"list": [{"key": 1}, {"key": 2}, {"key": 3}]} assert ( resolve_template( resource, - { - "result": "/Patient/{{ list.where($this = 0) }}/_history/{{ list.last() }}" - }, + {"result": "/Patient/{{ list.where($this = 0) }}/_history/{{ list.last() }}"}, ) == {} ) -def test_transformation_for_empty_array_nullable_template_expression_returns_null(): - resource = {"list": [{"key": 1}, {"key": 2}, {"key": 3}]} +def test_transformation_for_empty_array_nullable_template_expression_returns_null() -> None: + resource: Resource = {"list": [{"key": 1}, {"key": 2}, {"key": 3}]} assert resolve_template( resource, {"result": "/Patient/{{+ list.where($this = 0) +}}/_history/{{ list.last() }}"}, ) == {"result": None} -def test_transformation_for_multiline_template_works_properly(): - resource = {"list": [{"key": 1}]} +def test_transformation_for_multiline_template_works_properly() -> None: + resource: Resource = {"list": [{"key": 1}]} assert resolve_template(resource, "{{\nlist.where(\n$this.key=1\n).key\n}}") == 1 -def test_transformation_fails_with_incorrect_fhirpath_expression(): +def test_transformation_fails_with_incorrect_fhirpath_expression() -> None: with pytest.raises(FPMLValidationError): resolve_template({}, "{{ item.where(linkId='a) }}") def test_context_block_resolve_template() -> None: - resource = { + resource: Resource = { "foo": "bar", "list": [{"key": "a"}, {"key": "b"}, {"key": "c"}], } @@ -158,7 +155,7 @@ def test_context_block_resolve_template() -> None: def test_assign_block_single_var_as_object() -> None: - resource = { + resource: Resource = { "resourceType": "Resource", "sourceValue": 100, } @@ -172,7 +169,7 @@ def test_assign_block_single_var_as_object() -> None: def test_assign_block_multiple_vars_as_array_of_objects() -> None: - resource = { + resource: Resource = { "resourceType": "Resource", "sourceValue": 100, } @@ -190,7 +187,7 @@ def test_assign_block_multiple_vars_as_array_of_objects() -> None: def test_assign_block_isolated_nested_context() -> None: - resource = { + resource: Resource = { "resourceType": "Resource", "sourceValue": 100, } @@ -211,7 +208,7 @@ def test_assign_block_isolated_nested_context() -> None: def test_assign_block_full_example() -> None: - resource = { + resource: Resource = { "resourceType": "Resource", "sourceValue": 100, } @@ -221,9 +218,7 @@ def test_assign_block_full_example() -> None: "{% assign %}": [ { "varA": { - "{% assign %}": [ - {"varX": "{{ Resource.sourceValue.first() }}"} - ], + "{% assign %}": [{"varX": "{{ Resource.sourceValue.first() }}"}], "x": "{{ %varX }}", } }, @@ -253,7 +248,7 @@ def test_assign_block_full_example() -> None: def test_assign_block_fails_with_multiple_keys_in_object() -> None: - resource = { + resource: Resource = { "resourceType": "Resource", "sourceValue": 100, } @@ -268,7 +263,7 @@ def test_assign_block_fails_with_multiple_keys_in_object() -> None: def test_assign_block_fails_with_multiple_keys_in_array_of_objects() -> None: - resource = { + resource: Resource = { "resourceType": "Resource", "sourceValue": 100, } @@ -283,7 +278,7 @@ def test_assign_block_fails_with_multiple_keys_in_array_of_objects() -> None: def test_assign_block_fails_with_non_array_and_non_object_as_value() -> None: - resource = { + resource: Resource = { "resourceType": "Resource", "sourceValue": 100, } @@ -297,7 +292,7 @@ def test_assign_block_fails_with_non_array_and_non_object_as_value() -> None: ) -def test_for_block_resolve_template_full_example(): +def test_for_block_resolve_template_full_example() -> None: context = { "foo": "bar", "list": [{"key": "a"}, {"key": "b"}, {"key": "c"}], @@ -343,7 +338,7 @@ def test_for_block_resolve_template_full_example(): assert resolve_template(context, template) == expected_result -def test_for_block_resolve_template_local_assign(): +def test_for_block_resolve_template_local_assign() -> None: template = { "{% assign %}": { "localList": [{"key": "a"}, {"key": "b"}, {"key": "c"}], @@ -360,7 +355,7 @@ def test_for_block_resolve_template_local_assign(): assert resolve_template({}, template) == expected_result -def test_for_block_resolve_template_fails_with_other_keys(): +def test_for_block_resolve_template_fails_with_other_keys() -> None: context = {"list": [1, 2, 3]} template = { "userKey": 1, @@ -370,8 +365,8 @@ def test_for_block_resolve_template_fails_with_other_keys(): resolve_template(context, template) -def test_if_block_returns_if_branch_for_truthy_condition_at_root_level(): - resource = { +def test_if_block_returns_if_branch_for_truthy_condition_at_root_level() -> None: + resource: Resource = { "key": "value", } result = resolve_template( @@ -384,8 +379,8 @@ def test_if_block_returns_if_branch_for_truthy_condition_at_root_level(): assert result == {"nested": "truevalue"} -def test_if_block_returns_if_branch_for_truthy_condition(): - resource = { +def test_if_block_returns_if_branch_for_truthy_condition() -> None: + resource: Resource = { "key": "value", } result = resolve_template( @@ -400,8 +395,8 @@ def test_if_block_returns_if_branch_for_truthy_condition(): assert result == {"result": {"nested": "truevalue"}} -def test_if_block_returns_if_branch_for_truthy_condition_without_else_branch(): - resource = { +def test_if_block_returns_if_branch_for_truthy_condition_without_else_branch() -> None: + resource: Resource = { "key": "value", } result = resolve_template( @@ -415,8 +410,8 @@ def test_if_block_returns_if_branch_for_truthy_condition_without_else_branch(): assert result == {"result": {"nested": "truevalue"}} -def test_if_block_returns_else_branch_for_falsy_condition(): - resource = { +def test_if_block_returns_else_branch_for_falsy_condition() -> None: + resource: Resource = { "key": "value", } result = resolve_template( @@ -431,8 +426,8 @@ def test_if_block_returns_else_branch_for_falsy_condition(): assert result == {"result": {"nested": "falsevalue"}} -def test_if_block_clears_undefined_keys_for_falsy_condition_without_else_branch(): - resource = { +def test_if_block_clears_undefined_keys_for_falsy_condition_without_else_branch() -> None: + resource: Resource = { "key": "value", } result = resolve_template( @@ -446,8 +441,8 @@ def test_if_block_clears_undefined_keys_for_falsy_condition_without_else_branch( assert result == {} -def test_if_block_returns_null_for_falsy_condition_with_nullable_else_branch(): - resource = { +def test_if_block_returns_null_for_falsy_condition_with_nullable_else_branch() -> None: + resource: Resource = { "key": "value", } result = resolve_template( @@ -462,8 +457,8 @@ def test_if_block_returns_null_for_falsy_condition_with_nullable_else_branch(): assert result == {"result": None} -def test_if_block_returns_if_branch_for_nested_if(): - resource = { +def test_if_block_returns_if_branch_for_nested_if() -> None: + resource: Resource = { "key": "value", } result = resolve_template( @@ -479,8 +474,8 @@ def test_if_block_returns_if_branch_for_nested_if(): assert result == {"result": "value"} -def test_if_block_returns_else_branch_for_nested_else(): - resource = { +def test_if_block_returns_else_branch_for_nested_else() -> None: + resource: Resource = { "key": "value", } result = resolve_template( @@ -498,8 +493,8 @@ def test_if_block_returns_else_branch_for_nested_else(): assert result == {"result": "value"} -def test_if_block_implicitly_merges_with_null_returned(): - resource = { +def test_if_block_implicitly_merges_with_null_returned() -> None: + resource: Resource = { "key": "value", } result = resolve_template( @@ -514,8 +509,8 @@ def test_if_block_implicitly_merges_with_null_returned(): assert result == {"result": {"myKey": 1}} -def test_if_block_implicitly_merges_with_object_returned_for_truthy_condition(): - resource = { +def test_if_block_implicitly_merges_with_object_returned_for_truthy_condition() -> None: + resource: Resource = { "key": "value", } result = resolve_template( @@ -530,8 +525,8 @@ def test_if_block_implicitly_merges_with_object_returned_for_truthy_condition(): assert result == {"result": {"myKey": 1, "anotherKey": 2}} -def test_if_block_implicitly_merges_with_object_returned_for_falsy_condition(): - resource = { +def test_if_block_implicitly_merges_with_object_returned_for_falsy_condition() -> None: + resource: Resource = { "key": "value", } result = resolve_template( @@ -547,8 +542,8 @@ def test_if_block_implicitly_merges_with_object_returned_for_falsy_condition(): assert result == {"result": {"myKey": 1, "anotherKey": 3}} -def test_if_block_fails_on_implicit_merge_with_non_object_returned_from_if_branch(): - resource = { +def test_if_block_fails_on_implicit_merge_with_non_object_returned_from_if_branch() -> None: + resource: Resource = { "key": "value", } with pytest.raises(FPMLValidationError): @@ -563,8 +558,8 @@ def test_if_block_fails_on_implicit_merge_with_non_object_returned_from_if_branc ) -def test_if_block_fails_on_implicit_merge_with_non_object_returned_from_else_branch(): - resource = { +def test_if_block_fails_on_implicit_merge_with_non_object_returned_from_else_branch() -> None: + resource: Resource = { "key": "value", } with pytest.raises(FPMLValidationError): @@ -580,8 +575,8 @@ def test_if_block_fails_on_implicit_merge_with_non_object_returned_from_else_bra ) -def test_if_block_fails_on_multiple_if_blocks(): - resource = { +def test_if_block_fails_on_multiple_if_blocks() -> None: + resource: Resource = { "key": "value", } with pytest.raises(FPMLValidationError): @@ -597,8 +592,8 @@ def test_if_block_fails_on_multiple_if_blocks(): ) -def test_if_block_fails_on_multiple_else_blocks(): - resource = { +def test_if_block_fails_on_multiple_else_blocks() -> None: + resource: Resource = { "key": "value", } with pytest.raises(FPMLValidationError): @@ -615,8 +610,8 @@ def test_if_block_fails_on_multiple_else_blocks(): ) -def test_if_block_fails_on_else_block_without_if_block(): - resource = { +def test_if_block_fails_on_else_block_without_if_block() -> None: + resource: Resource = { "key": "value", } with pytest.raises(FPMLValidationError): @@ -631,8 +626,8 @@ def test_if_block_fails_on_else_block_without_if_block(): ) -def test_merge_block_implicitly_merges_into_the_node(): - resource = { +def test_merge_block_implicitly_merges_into_the_node() -> None: + resource: Resource = { "key": "value", } result = resolve_template( @@ -645,8 +640,8 @@ def test_merge_block_implicitly_merges_into_the_node(): assert result == {"b": 1, "a": 1} -def test_merge_block_merges_multiple_blocks_within_order(): - resource = { +def test_merge_block_merges_multiple_blocks_within_order() -> None: + resource: Resource = { "key": "value", } result = resolve_template( @@ -658,8 +653,8 @@ def test_merge_block_merges_multiple_blocks_within_order(): assert result == {"a": 3, "b": 2} -def test_merge_block_merges_multiple_blocks_containing_nulls(): - resource = { +def test_merge_block_merges_multiple_blocks_containing_nulls() -> None: + resource: Resource = { "key": "value", } result = resolve_template( @@ -671,8 +666,8 @@ def test_merge_block_merges_multiple_blocks_containing_nulls(): assert result == {"a": 1, "b": 2} -def test_merge_block_merges_multiple_blocks_containing_undefined(): - resource = { +def test_merge_block_merges_multiple_blocks_containing_undefined() -> None: + resource: Resource = { "key": "value", } result = resolve_template( @@ -684,8 +679,8 @@ def test_merge_block_merges_multiple_blocks_containing_undefined(): assert result == {"a": 1, "b": 2} -def test_merge_block_fails_on_merge_with_non_object(): - resource = { +def test_merge_block_fails_on_merge_with_non_object() -> None: + resource: Resource = { "key": "value", } with pytest.raises(FPMLValidationError): From 1d397aeda73ee8111b5c55ee50be2c6a1b37d31c Mon Sep 17 00:00:00 2001 From: Vadim Laletin Date: Fri, 28 Feb 2025 12:51:59 +0100 Subject: [PATCH 12/23] Add autohooks --- python/.gitignore | 1 - python/poetry.lock | 821 ++++++++++++++++++++++++++++++++++++++++++ python/pyproject.toml | 10 +- 3 files changed, 829 insertions(+), 3 deletions(-) create mode 100644 python/poetry.lock diff --git a/python/.gitignore b/python/.gitignore index a997604..cbb5790 100644 --- a/python/.gitignore +++ b/python/.gitignore @@ -1,4 +1,3 @@ -poetry.lock dev.code-workspace .mypy_cache __pycache__ diff --git a/python/poetry.lock b/python/poetry.lock new file mode 100644 index 0000000..8589cc4 --- /dev/null +++ b/python/poetry.lock @@ -0,0 +1,821 @@ +# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. + +[[package]] +name = "antlr4-python3-runtime" +version = "4.13.2" +description = "ANTLR 4.13.2 runtime for Python 3" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "antlr4_python3_runtime-4.13.2-py3-none-any.whl", hash = "sha256:fe3835eb8d33daece0e799090eda89719dbccee7aa39ef94eed3818cafa5a7e8"}, + {file = "antlr4_python3_runtime-4.13.2.tar.gz", hash = "sha256:909b647e1d2fc2b70180ac586df3933e38919c85f98ccc656a96cd3f25ef3916"}, +] + +[[package]] +name = "anyio" +version = "4.8.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a"}, + {file = "anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" +typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} + +[package.extras] +doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""] +trio = ["trio (>=0.26.1)"] + +[[package]] +name = "autohooks" +version = "25.2.0" +description = "Library for managing git hooks" +optional = false +python-versions = "<4.0,>=3.9" +groups = ["dev"] +files = [ + {file = "autohooks-25.2.0-py3-none-any.whl", hash = "sha256:5a2ed5fc03d899725638d2ca6bcb367f025a6196b225a5ea177e502fdf1bb065"}, + {file = "autohooks-25.2.0.tar.gz", hash = "sha256:e306a24801fbc815628ebe48b063e2853a0857eb00bbdae733175e06e0ce8a84"}, +] + +[package.dependencies] +pontos = ">=22.8.0" +rich = ">=12.5.1" +shtab = ">=1.7.0" +tomlkit = ">=0.5.11" + +[[package]] +name = "autohooks-plugin-mypy" +version = "23.10.0" +description = "An autohooks plugin for python code static typing check with mypy" +optional = false +python-versions = ">=3.9,<4.0" +groups = ["dev"] +files = [ + {file = "autohooks_plugin_mypy-23.10.0-py3-none-any.whl", hash = "sha256:8ac36b74900b2f2456fec046126e564374acd6de2752d87255c6f71c4e6a73ff"}, + {file = "autohooks_plugin_mypy-23.10.0.tar.gz", hash = "sha256:ebefaa83074b662de38c914f6cac9f4f8e3452e36f54a5834df3f1590cc0c540"}, +] + +[package.dependencies] +autohooks = ">=21.7.0" +mypy = ">=0.910" + +[[package]] +name = "autohooks-plugin-ruff" +version = "25.2.0" +description = "An autohooks plugin for python code formatting via ruff" +optional = false +python-versions = "<4.0,>=3.9" +groups = ["dev"] +files = [ + {file = "autohooks_plugin_ruff-25.2.0-py3-none-any.whl", hash = "sha256:b0e3bbfb2d8bb94c509dad291801af1f6821877147b7f02f7e93812f8aef6ba1"}, + {file = "autohooks_plugin_ruff-25.2.0.tar.gz", hash = "sha256:3290cd5b1939d80113e5a3b6408e4728e1c2bed0960b8101479897a196dc69d6"}, +] + +[package.dependencies] +autohooks = ">=25.2.0" +ruff = ">=0.0.272" + +[[package]] +name = "certifi" +version = "2025.1.31" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, + {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev", "test"] +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] +markers = {dev = "platform_system == \"Windows\"", test = "sys_platform == \"win32\""} + +[[package]] +name = "colorful" +version = "0.5.6" +description = "Terminal string styling done right, in Python." +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "colorful-0.5.6-py2.py3-none-any.whl", hash = "sha256:eab8c1c809f5025ad2b5238a50bd691e26850da8cac8f90d660ede6ea1af9f1e"}, + {file = "colorful-0.5.6.tar.gz", hash = "sha256:b56d5c01db1dac4898308ea889edcb113fbee3e6ec5df4bacffd61d5241b5b8d"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +groups = ["dev", "test"] +markers = "python_version < \"3.11\"" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "fhirpathpy" +version = "1.2.1" +description = "FHIRPath implementation in Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "fhirpathpy-1.2.1-py3-none-any.whl", hash = "sha256:b45c957d2de9e35c9eb19425de29cbc2e3f73d324576da09f305610e78153902"}, + {file = "fhirpathpy-1.2.1.tar.gz", hash = "sha256:1e82a5199d6f63342a1f56b856a23b76875b4209ab9d29ab659b7bba686089cc"}, +] + +[package.dependencies] +antlr4-python3-runtime = ">=4.10,<5.0" +python-dateutil = ">=2.8,<3.0" + +[package.extras] +test = ["pytest (==7.1.1)", "pyyaml (==5.4)"] + +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "h2" +version = "4.2.0" +description = "Pure-Python HTTP/2 protocol implementation" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "h2-4.2.0-py3-none-any.whl", hash = "sha256:479a53ad425bb29af087f3458a61d30780bc818e4ebcf01f0b536ba916462ed0"}, + {file = "h2-4.2.0.tar.gz", hash = "sha256:c8a52129695e88b1a0578d8d2cc6842bbd79128ac685463b887ee278126ad01f"}, +] + +[package.dependencies] +hpack = ">=4.1,<5" +hyperframe = ">=6.1,<7" + +[[package]] +name = "hpack" +version = "4.1.0" +description = "Pure-Python HPACK header encoding" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496"}, + {file = "hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca"}, +] + +[[package]] +name = "httpcore" +version = "1.0.7" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"}, + {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.13,<0.15" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<1.0)"] + +[[package]] +name = "httpx" +version = "0.28.1" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +h2 = {version = ">=3,<5", optional = true, markers = "extra == \"http2\""} +httpcore = "==1.*" +idna = "*" + +[package.extras] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "hyperframe" +version = "6.1.0" +description = "Pure-Python HTTP/2 framing" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5"}, + {file = "hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08"}, +] + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +groups = ["test"] +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "lxml" +version = "5.3.1" +description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "lxml-5.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a4058f16cee694577f7e4dd410263cd0ef75644b43802a689c2b3c2a7e69453b"}, + {file = "lxml-5.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:364de8f57d6eda0c16dcfb999af902da31396949efa0e583e12675d09709881b"}, + {file = "lxml-5.3.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:528f3a0498a8edc69af0559bdcf8a9f5a8bf7c00051a6ef3141fdcf27017bbf5"}, + {file = "lxml-5.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db4743e30d6f5f92b6d2b7c86b3ad250e0bad8dee4b7ad8a0c44bfb276af89a3"}, + {file = "lxml-5.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:17b5d7f8acf809465086d498d62a981fa6a56d2718135bb0e4aa48c502055f5c"}, + {file = "lxml-5.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:928e75a7200a4c09e6efc7482a1337919cc61fe1ba289f297827a5b76d8969c2"}, + {file = "lxml-5.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a997b784a639e05b9d4053ef3b20c7e447ea80814a762f25b8ed5a89d261eac"}, + {file = "lxml-5.3.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:7b82e67c5feb682dbb559c3e6b78355f234943053af61606af126df2183b9ef9"}, + {file = "lxml-5.3.1-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:f1de541a9893cf8a1b1db9bf0bf670a2decab42e3e82233d36a74eda7822b4c9"}, + {file = "lxml-5.3.1-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:de1fc314c3ad6bc2f6bd5b5a5b9357b8c6896333d27fdbb7049aea8bd5af2d79"}, + {file = "lxml-5.3.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:7c0536bd9178f754b277a3e53f90f9c9454a3bd108b1531ffff720e082d824f2"}, + {file = "lxml-5.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:68018c4c67d7e89951a91fbd371e2e34cd8cfc71f0bb43b5332db38497025d51"}, + {file = "lxml-5.3.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:aa826340a609d0c954ba52fd831f0fba2a4165659ab0ee1a15e4aac21f302406"}, + {file = "lxml-5.3.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:796520afa499732191e39fc95b56a3b07f95256f2d22b1c26e217fb69a9db5b5"}, + {file = "lxml-5.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3effe081b3135237da6e4c4530ff2a868d3f80be0bda027e118a5971285d42d0"}, + {file = "lxml-5.3.1-cp310-cp310-win32.whl", hash = "sha256:a22f66270bd6d0804b02cd49dae2b33d4341015545d17f8426f2c4e22f557a23"}, + {file = "lxml-5.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:0bcfadea3cdc68e678d2b20cb16a16716887dd00a881e16f7d806c2138b8ff0c"}, + {file = "lxml-5.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e220f7b3e8656ab063d2eb0cd536fafef396829cafe04cb314e734f87649058f"}, + {file = "lxml-5.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f2cfae0688fd01f7056a17367e3b84f37c545fb447d7282cf2c242b16262607"}, + {file = "lxml-5.3.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:67d2f8ad9dcc3a9e826bdc7802ed541a44e124c29b7d95a679eeb58c1c14ade8"}, + {file = "lxml-5.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db0c742aad702fd5d0c6611a73f9602f20aec2007c102630c06d7633d9c8f09a"}, + {file = "lxml-5.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:198bb4b4dd888e8390afa4f170d4fa28467a7eaf857f1952589f16cfbb67af27"}, + {file = "lxml-5.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2a3e412ce1849be34b45922bfef03df32d1410a06d1cdeb793a343c2f1fd666"}, + {file = "lxml-5.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b8969dbc8d09d9cd2ae06362c3bad27d03f433252601ef658a49bd9f2b22d79"}, + {file = "lxml-5.3.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5be8f5e4044146a69c96077c7e08f0709c13a314aa5315981185c1f00235fe65"}, + {file = "lxml-5.3.1-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:133f3493253a00db2c870d3740bc458ebb7d937bd0a6a4f9328373e0db305709"}, + {file = "lxml-5.3.1-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:52d82b0d436edd6a1d22d94a344b9a58abd6c68c357ed44f22d4ba8179b37629"}, + {file = "lxml-5.3.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1b6f92e35e2658a5ed51c6634ceb5ddae32053182851d8cad2a5bc102a359b33"}, + {file = "lxml-5.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:203b1d3eaebd34277be06a3eb880050f18a4e4d60861efba4fb946e31071a295"}, + {file = "lxml-5.3.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:155e1a5693cf4b55af652f5c0f78ef36596c7f680ff3ec6eb4d7d85367259b2c"}, + {file = "lxml-5.3.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22ec2b3c191f43ed21f9545e9df94c37c6b49a5af0a874008ddc9132d49a2d9c"}, + {file = "lxml-5.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7eda194dd46e40ec745bf76795a7cccb02a6a41f445ad49d3cf66518b0bd9cff"}, + {file = "lxml-5.3.1-cp311-cp311-win32.whl", hash = "sha256:fb7c61d4be18e930f75948705e9718618862e6fc2ed0d7159b2262be73f167a2"}, + {file = "lxml-5.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:c809eef167bf4a57af4b03007004896f5c60bd38dc3852fcd97a26eae3d4c9e6"}, + {file = "lxml-5.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e69add9b6b7b08c60d7ff0152c7c9a6c45b4a71a919be5abde6f98f1ea16421c"}, + {file = "lxml-5.3.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4e52e1b148867b01c05e21837586ee307a01e793b94072d7c7b91d2c2da02ffe"}, + {file = "lxml-5.3.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4b382e0e636ed54cd278791d93fe2c4f370772743f02bcbe431a160089025c9"}, + {file = "lxml-5.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2e49dc23a10a1296b04ca9db200c44d3eb32c8d8ec532e8c1fd24792276522a"}, + {file = "lxml-5.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4399b4226c4785575fb20998dc571bc48125dc92c367ce2602d0d70e0c455eb0"}, + {file = "lxml-5.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5412500e0dc5481b1ee9cf6b38bb3b473f6e411eb62b83dc9b62699c3b7b79f7"}, + {file = "lxml-5.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c93ed3c998ea8472be98fb55aed65b5198740bfceaec07b2eba551e55b7b9ae"}, + {file = "lxml-5.3.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:63d57fc94eb0bbb4735e45517afc21ef262991d8758a8f2f05dd6e4174944519"}, + {file = "lxml-5.3.1-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:b450d7cabcd49aa7ab46a3c6aa3ac7e1593600a1a0605ba536ec0f1b99a04322"}, + {file = "lxml-5.3.1-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:4df0ec814b50275ad6a99bc82a38b59f90e10e47714ac9871e1b223895825468"}, + {file = "lxml-5.3.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d184f85ad2bb1f261eac55cddfcf62a70dee89982c978e92b9a74a1bfef2e367"}, + {file = "lxml-5.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b725e70d15906d24615201e650d5b0388b08a5187a55f119f25874d0103f90dd"}, + {file = "lxml-5.3.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a31fa7536ec1fb7155a0cd3a4e3d956c835ad0a43e3610ca32384d01f079ea1c"}, + {file = "lxml-5.3.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3c3c8b55c7fc7b7e8877b9366568cc73d68b82da7fe33d8b98527b73857a225f"}, + {file = "lxml-5.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d61ec60945d694df806a9aec88e8f29a27293c6e424f8ff91c80416e3c617645"}, + {file = "lxml-5.3.1-cp312-cp312-win32.whl", hash = "sha256:f4eac0584cdc3285ef2e74eee1513a6001681fd9753b259e8159421ed28a72e5"}, + {file = "lxml-5.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:29bfc8d3d88e56ea0a27e7c4897b642706840247f59f4377d81be8f32aa0cfbf"}, + {file = "lxml-5.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c093c7088b40d8266f57ed71d93112bd64c6724d31f0794c1e52cc4857c28e0e"}, + {file = "lxml-5.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b0884e3f22d87c30694e625b1e62e6f30d39782c806287450d9dc2fdf07692fd"}, + {file = "lxml-5.3.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1637fa31ec682cd5760092adfabe86d9b718a75d43e65e211d5931809bc111e7"}, + {file = "lxml-5.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a364e8e944d92dcbf33b6b494d4e0fb3499dcc3bd9485beb701aa4b4201fa414"}, + {file = "lxml-5.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:779e851fd0e19795ccc8a9bb4d705d6baa0ef475329fe44a13cf1e962f18ff1e"}, + {file = "lxml-5.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c4393600915c308e546dc7003d74371744234e8444a28622d76fe19b98fa59d1"}, + {file = "lxml-5.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:673b9d8e780f455091200bba8534d5f4f465944cbdd61f31dc832d70e29064a5"}, + {file = "lxml-5.3.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:2e4a570f6a99e96c457f7bec5ad459c9c420ee80b99eb04cbfcfe3fc18ec6423"}, + {file = "lxml-5.3.1-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:71f31eda4e370f46af42fc9f264fafa1b09f46ba07bdbee98f25689a04b81c20"}, + {file = "lxml-5.3.1-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:42978a68d3825eaac55399eb37a4d52012a205c0c6262199b8b44fcc6fd686e8"}, + {file = "lxml-5.3.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:8b1942b3e4ed9ed551ed3083a2e6e0772de1e5e3aca872d955e2e86385fb7ff9"}, + {file = "lxml-5.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:85c4f11be9cf08917ac2a5a8b6e1ef63b2f8e3799cec194417e76826e5f1de9c"}, + {file = "lxml-5.3.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:231cf4d140b22a923b1d0a0a4e0b4f972e5893efcdec188934cc65888fd0227b"}, + {file = "lxml-5.3.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5865b270b420eda7b68928d70bb517ccbe045e53b1a428129bb44372bf3d7dd5"}, + {file = "lxml-5.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dbf7bebc2275016cddf3c997bf8a0f7044160714c64a9b83975670a04e6d2252"}, + {file = "lxml-5.3.1-cp313-cp313-win32.whl", hash = "sha256:d0751528b97d2b19a388b302be2a0ee05817097bab46ff0ed76feeec24951f78"}, + {file = "lxml-5.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:91fb6a43d72b4f8863d21f347a9163eecbf36e76e2f51068d59cd004c506f332"}, + {file = "lxml-5.3.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:016b96c58e9a4528219bb563acf1aaaa8bc5452e7651004894a973f03b84ba81"}, + {file = "lxml-5.3.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82a4bb10b0beef1434fb23a09f001ab5ca87895596b4581fd53f1e5145a8934a"}, + {file = "lxml-5.3.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d68eeef7b4d08a25e51897dac29bcb62aba830e9ac6c4e3297ee7c6a0cf6439"}, + {file = "lxml-5.3.1-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:f12582b8d3b4c6be1d298c49cb7ae64a3a73efaf4c2ab4e37db182e3545815ac"}, + {file = "lxml-5.3.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2df7ed5edeb6bd5590914cd61df76eb6cce9d590ed04ec7c183cf5509f73530d"}, + {file = "lxml-5.3.1-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:585c4dc429deebc4307187d2b71ebe914843185ae16a4d582ee030e6cfbb4d8a"}, + {file = "lxml-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:06a20d607a86fccab2fc15a77aa445f2bdef7b49ec0520a842c5c5afd8381576"}, + {file = "lxml-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:057e30d0012439bc54ca427a83d458752ccda725c1c161cc283db07bcad43cf9"}, + {file = "lxml-5.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4867361c049761a56bd21de507cab2c2a608c55102311d142ade7dab67b34f32"}, + {file = "lxml-5.3.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3dddf0fb832486cc1ea71d189cb92eb887826e8deebe128884e15020bb6e3f61"}, + {file = "lxml-5.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bcc211542f7af6f2dfb705f5f8b74e865592778e6cafdfd19c792c244ccce19"}, + {file = "lxml-5.3.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaca5a812f050ab55426c32177091130b1e49329b3f002a32934cd0245571307"}, + {file = "lxml-5.3.1-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:236610b77589faf462337b3305a1be91756c8abc5a45ff7ca8f245a71c5dab70"}, + {file = "lxml-5.3.1-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:aed57b541b589fa05ac248f4cb1c46cbb432ab82cbd467d1c4f6a2bdc18aecf9"}, + {file = "lxml-5.3.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:75fa3d6946d317ffc7016a6fcc44f42db6d514b7fdb8b4b28cbe058303cb6e53"}, + {file = "lxml-5.3.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:96eef5b9f336f623ffc555ab47a775495e7e8846dde88de5f941e2906453a1ce"}, + {file = "lxml-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:ef45f31aec9be01379fc6c10f1d9c677f032f2bac9383c827d44f620e8a88407"}, + {file = "lxml-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0611da6b07dd3720f492db1b463a4d1175b096b49438761cc9f35f0d9eaaef5"}, + {file = "lxml-5.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b2aca14c235c7a08558fe0a4786a1a05873a01e86b474dfa8f6df49101853a4e"}, + {file = "lxml-5.3.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae82fce1d964f065c32c9517309f0c7be588772352d2f40b1574a214bd6e6098"}, + {file = "lxml-5.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7aae7a3d63b935babfdc6864b31196afd5145878ddd22f5200729006366bc4d5"}, + {file = "lxml-5.3.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8e0d177b1fe251c3b1b914ab64135475c5273c8cfd2857964b2e3bb0fe196a7"}, + {file = "lxml-5.3.1-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:6c4dd3bfd0c82400060896717dd261137398edb7e524527438c54a8c34f736bf"}, + {file = "lxml-5.3.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:f1208c1c67ec9e151d78aa3435aa9b08a488b53d9cfac9b699f15255a3461ef2"}, + {file = "lxml-5.3.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:c6aacf00d05b38a5069826e50ae72751cb5bc27bdc4d5746203988e429b385bb"}, + {file = "lxml-5.3.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5881aaa4bf3a2d086c5f20371d3a5856199a0d8ac72dd8d0dbd7a2ecfc26ab73"}, + {file = "lxml-5.3.1-cp38-cp38-win32.whl", hash = "sha256:45fbb70ccbc8683f2fb58bea89498a7274af1d9ec7995e9f4af5604e028233fc"}, + {file = "lxml-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:7512b4d0fc5339d5abbb14d1843f70499cab90d0b864f790e73f780f041615d7"}, + {file = "lxml-5.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5885bc586f1edb48e5d68e7a4b4757b5feb2a496b64f462b4d65950f5af3364f"}, + {file = "lxml-5.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1b92fe86e04f680b848fff594a908edfa72b31bfc3499ef7433790c11d4c8cd8"}, + {file = "lxml-5.3.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a091026c3bf7519ab1e64655a3f52a59ad4a4e019a6f830c24d6430695b1cf6a"}, + {file = "lxml-5.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ffb141361108e864ab5f1813f66e4e1164181227f9b1f105b042729b6c15125"}, + {file = "lxml-5.3.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3715cdf0dd31b836433af9ee9197af10e3df41d273c19bb249230043667a5dfd"}, + {file = "lxml-5.3.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88b72eb7222d918c967202024812c2bfb4048deeb69ca328363fb8e15254c549"}, + {file = "lxml-5.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa59974880ab5ad8ef3afaa26f9bda148c5f39e06b11a8ada4660ecc9fb2feb3"}, + {file = "lxml-5.3.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:3bb8149840daf2c3f97cebf00e4ed4a65a0baff888bf2605a8d0135ff5cf764e"}, + {file = "lxml-5.3.1-cp39-cp39-manylinux_2_28_ppc64le.whl", hash = "sha256:0d6b2fa86becfa81f0a0271ccb9eb127ad45fb597733a77b92e8a35e53414914"}, + {file = "lxml-5.3.1-cp39-cp39-manylinux_2_28_s390x.whl", hash = "sha256:136bf638d92848a939fd8f0e06fcf92d9f2e4b57969d94faae27c55f3d85c05b"}, + {file = "lxml-5.3.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:89934f9f791566e54c1d92cdc8f8fd0009447a5ecdb1ec6b810d5f8c4955f6be"}, + {file = "lxml-5.3.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a8ade0363f776f87f982572c2860cc43c65ace208db49c76df0a21dde4ddd16e"}, + {file = "lxml-5.3.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:bfbbab9316330cf81656fed435311386610f78b6c93cc5db4bebbce8dd146675"}, + {file = "lxml-5.3.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:172d65f7c72a35a6879217bcdb4bb11bc88d55fb4879e7569f55616062d387c2"}, + {file = "lxml-5.3.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e3c623923967f3e5961d272718655946e5322b8d058e094764180cdee7bab1af"}, + {file = "lxml-5.3.1-cp39-cp39-win32.whl", hash = "sha256:ce0930a963ff593e8bb6fda49a503911accc67dee7e5445eec972668e672a0f0"}, + {file = "lxml-5.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:f7b64fcd670bca8800bc10ced36620c6bbb321e7bc1214b9c0c0df269c1dddc2"}, + {file = "lxml-5.3.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:afa578b6524ff85fb365f454cf61683771d0170470c48ad9d170c48075f86725"}, + {file = "lxml-5.3.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f5e80adf0aafc7b5454f2c1cb0cde920c9b1f2cbd0485f07cc1d0497c35c5d"}, + {file = "lxml-5.3.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dd0b80ac2d8f13ffc906123a6f20b459cb50a99222d0da492360512f3e50f84"}, + {file = "lxml-5.3.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:422c179022ecdedbe58b0e242607198580804253da220e9454ffe848daa1cfd2"}, + {file = "lxml-5.3.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:524ccfded8989a6595dbdda80d779fb977dbc9a7bc458864fc9a0c2fc15dc877"}, + {file = "lxml-5.3.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:48fd46bf7155def2e15287c6f2b133a2f78e2d22cdf55647269977b873c65499"}, + {file = "lxml-5.3.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:05123fad495a429f123307ac6d8fd6f977b71e9a0b6d9aeeb8f80c017cb17131"}, + {file = "lxml-5.3.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a243132767150a44e6a93cd1dde41010036e1cbc63cc3e9fe1712b277d926ce3"}, + {file = "lxml-5.3.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c92ea6d9dd84a750b2bae72ff5e8cf5fdd13e58dda79c33e057862c29a8d5b50"}, + {file = "lxml-5.3.1-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:2f1be45d4c15f237209bbf123a0e05b5d630c8717c42f59f31ea9eae2ad89394"}, + {file = "lxml-5.3.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:a83d3adea1e0ee36dac34627f78ddd7f093bb9cfc0a8e97f1572a949b695cb98"}, + {file = "lxml-5.3.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:3edbb9c9130bac05d8c3fe150c51c337a471cc7fdb6d2a0a7d3a88e88a829314"}, + {file = "lxml-5.3.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2f23cf50eccb3255b6e913188291af0150d89dab44137a69e14e4dcb7be981f1"}, + {file = "lxml-5.3.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df7e5edac4778127f2bf452e0721a58a1cfa4d1d9eac63bdd650535eb8543615"}, + {file = "lxml-5.3.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:094b28ed8a8a072b9e9e2113a81fda668d2053f2ca9f2d202c2c8c7c2d6516b1"}, + {file = "lxml-5.3.1-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:514fe78fc4b87e7a7601c92492210b20a1b0c6ab20e71e81307d9c2e377c64de"}, + {file = "lxml-5.3.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8fffc08de02071c37865a155e5ea5fce0282e1546fd5bde7f6149fcaa32558ac"}, + {file = "lxml-5.3.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4b0d5cdba1b655d5b18042ac9c9ff50bda33568eb80feaaca4fc237b9c4fbfde"}, + {file = "lxml-5.3.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3031e4c16b59424e8d78522c69b062d301d951dc55ad8685736c3335a97fc270"}, + {file = "lxml-5.3.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb659702a45136c743bc130760c6f137870d4df3a9e14386478b8a0511abcfca"}, + {file = "lxml-5.3.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a11b16a33656ffc43c92a5343a28dc71eefe460bcc2a4923a96f292692709f6"}, + {file = "lxml-5.3.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c5ae125276f254b01daa73e2c103363d3e99e3e10505686ac7d9d2442dd4627a"}, + {file = "lxml-5.3.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c76722b5ed4a31ba103e0dc77ab869222ec36efe1a614e42e9bcea88a36186fe"}, + {file = "lxml-5.3.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:33e06717c00c788ab4e79bc4726ecc50c54b9bfb55355eae21473c145d83c2d2"}, + {file = "lxml-5.3.1.tar.gz", hash = "sha256:106b7b5d2977b339f1e97efe2778e2ab20e99994cbb0ec5e55771ed0795920c8"}, +] + +[package.extras] +cssselect = ["cssselect (>=0.7)"] +html-clean = ["lxml_html_clean"] +html5 = ["html5lib"] +htmlsoup = ["BeautifulSoup4"] +source = ["Cython (>=3.0.11,<3.1.0)"] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "mypy" +version = "1.15.0" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13"}, + {file = "mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559"}, + {file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b"}, + {file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3"}, + {file = "mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b"}, + {file = "mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828"}, + {file = "mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f"}, + {file = "mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5"}, + {file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e"}, + {file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c"}, + {file = "mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f"}, + {file = "mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f"}, + {file = "mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd"}, + {file = "mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f"}, + {file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464"}, + {file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee"}, + {file = "mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e"}, + {file = "mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22"}, + {file = "mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445"}, + {file = "mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d"}, + {file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5"}, + {file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036"}, + {file = "mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357"}, + {file = "mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf"}, + {file = "mypy-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e601a7fa172c2131bff456bb3ee08a88360760d0d2f8cbd7a75a65497e2df078"}, + {file = "mypy-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:712e962a6357634fef20412699a3655c610110e01cdaa6180acec7fc9f8513ba"}, + {file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95579473af29ab73a10bada2f9722856792a36ec5af5399b653aa28360290a5"}, + {file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f8722560a14cde92fdb1e31597760dc35f9f5524cce17836c0d22841830fd5b"}, + {file = "mypy-1.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fbb8da62dc352133d7d7ca90ed2fb0e9d42bb1a32724c287d3c76c58cbaa9c2"}, + {file = "mypy-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:d10d994b41fb3497719bbf866f227b3489048ea4bbbb5015357db306249f7980"}, + {file = "mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e"}, + {file = "mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43"}, +] + +[package.dependencies] +mypy_extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing_extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +groups = ["dev"] +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "packaging" +version = "24.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["dev", "test"] +files = [ + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +groups = ["test"] +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pontos" +version = "25.1.0" +description = "Common utilities and tools maintained by Greenbone Networks" +optional = false +python-versions = "<4.0,>=3.9" +groups = ["dev"] +files = [ + {file = "pontos-25.1.0-py3-none-any.whl", hash = "sha256:952764e1b6aa9c1b7436e7fba71e64e6726d04a0a84925d51940998d9c5af667"}, + {file = "pontos-25.1.0.tar.gz", hash = "sha256:3a2a4267521e316fbac18aa21e7bb779358b1cb47e364ce7869ac4c630621594"}, +] + +[package.dependencies] +colorful = ">=0.5.4" +httpx = {version = ">=0.23", extras = ["http2"]} +lxml = ">=4.9.0" +packaging = ">=20.3" +python-dateutil = ">=2.8.2" +rich = ">=12.4.4" +semver = ">=2.13" +shtab = ">=1.7.0" +tomlkit = ">=0.5.11" + +[[package]] +name = "pygments" +version = "2.19.1" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, + {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pytest" +version = "8.3.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +groups = ["test"] +files = [ + {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, + {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main", "dev"] +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "rich" +version = "13.9.4" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.8.0" +groups = ["dev"] +files = [ + {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, + {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "ruff" +version = "0.9.7" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "ruff-0.9.7-py3-none-linux_armv6l.whl", hash = "sha256:99d50def47305fe6f233eb8dabfd60047578ca87c9dcb235c9723ab1175180f4"}, + {file = "ruff-0.9.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d59105ae9c44152c3d40a9c40d6331a7acd1cdf5ef404fbe31178a77b174ea66"}, + {file = "ruff-0.9.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f313b5800483770bd540cddac7c90fc46f895f427b7820f18fe1822697f1fec9"}, + {file = "ruff-0.9.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:042ae32b41343888f59c0a4148f103208bf6b21c90118d51dc93a68366f4e903"}, + {file = "ruff-0.9.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87862589373b33cc484b10831004e5e5ec47dc10d2b41ba770e837d4f429d721"}, + {file = "ruff-0.9.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a17e1e01bee0926d351a1ee9bc15c445beae888f90069a6192a07a84af544b6b"}, + {file = "ruff-0.9.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7c1f880ac5b2cbebd58b8ebde57069a374865c73f3bf41f05fe7a179c1c8ef22"}, + {file = "ruff-0.9.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e63fc20143c291cab2841dbb8260e96bafbe1ba13fd3d60d28be2c71e312da49"}, + {file = "ruff-0.9.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:91ff963baed3e9a6a4eba2a02f4ca8eaa6eba1cc0521aec0987da8d62f53cbef"}, + {file = "ruff-0.9.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88362e3227c82f63eaebf0b2eff5b88990280fb1ecf7105523883ba8c3aaf6fb"}, + {file = "ruff-0.9.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0372c5a90349f00212270421fe91874b866fd3626eb3b397ede06cd385f6f7e0"}, + {file = "ruff-0.9.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d76b8ab60e99e6424cd9d3d923274a1324aefce04f8ea537136b8398bbae0a62"}, + {file = "ruff-0.9.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0c439bdfc8983e1336577f00e09a4e7a78944fe01e4ea7fe616d00c3ec69a3d0"}, + {file = "ruff-0.9.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:115d1f15e8fdd445a7b4dc9a30abae22de3f6bcabeb503964904471691ef7606"}, + {file = "ruff-0.9.7-py3-none-win32.whl", hash = "sha256:e9ece95b7de5923cbf38893f066ed2872be2f2f477ba94f826c8defdd6ec6b7d"}, + {file = "ruff-0.9.7-py3-none-win_amd64.whl", hash = "sha256:3770fe52b9d691a15f0b87ada29c45324b2ace8f01200fb0c14845e499eb0c2c"}, + {file = "ruff-0.9.7-py3-none-win_arm64.whl", hash = "sha256:b075a700b2533feb7a01130ff656a4ec0d5f340bb540ad98759b8401c32c2037"}, + {file = "ruff-0.9.7.tar.gz", hash = "sha256:643757633417907510157b206e490c3aa11cab0c087c912f60e07fbafa87a4c6"}, +] + +[[package]] +name = "semver" +version = "3.0.4" +description = "Python helper for Semantic Versioning (https://semver.org)" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "semver-3.0.4-py3-none-any.whl", hash = "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746"}, + {file = "semver-3.0.4.tar.gz", hash = "sha256:afc7d8c584a5ed0a11033af086e8af226a9c0b206f313e0301f8dd7b6b589602"}, +] + +[[package]] +name = "shtab" +version = "1.7.1" +description = "Automagic shell tab completion for Python CLI applications" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "shtab-1.7.1-py3-none-any.whl", hash = "sha256:32d3d2ff9022d4c77a62492b6ec875527883891e33c6b479ba4d41a51e259983"}, + {file = "shtab-1.7.1.tar.gz", hash = "sha256:4e4bcb02eeb82ec45920a5d0add92eac9c9b63b2804c9196c1f1fdc2d039243c"}, +] + +[package.extras] +dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout"] + +[[package]] +name = "six" +version = "1.17.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main", "dev"] +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "tomli" +version = "2.2.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +groups = ["dev", "test"] +markers = "python_version < \"3.11\"" +files = [ + {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]] +name = "tomlkit" +version = "0.13.2" +description = "Style preserving TOML library" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, + {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[metadata] +lock-version = "2.1" +python-versions = ">=3.9,<4.0" +content-hash = "78afdf4908fd347861ffe496796fc5abac84e70dd3130107f8d074b91a7c146c" diff --git a/python/pyproject.toml b/python/pyproject.toml index f516bf5..5d9e755 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -43,6 +43,9 @@ python = ">=3.9,<4.0" [tool.poetry.group.dev.dependencies] ruff = "^0.9.7" mypy = "^1.15.0" +autohooks-plugin-mypy = "^23.10.0" +autohooks-plugin-ruff = "^25.2.0" +autohooks = "^25.2.0" [tool.poetry.group.test.dependencies] pytest = "^8.3.4" @@ -58,8 +61,11 @@ line-length = 100 [tool.ruff.lint] select = ["I", "E", "F", "N", "B", "C4", "PT", "UP", "I001", "A", "RET", "TID251", "RUF", "SIM", "PYI", "T20", "PIE", "G", "ISC", "PL"] - [tool.mypy] files = ["fpml", "tests"] ignore_missing_imports = true -check_untyped_defs = true \ No newline at end of file +check_untyped_defs = true + +[tool.autohooks] +mode = "poetry" +pre-commit = ["autohooks.plugins.mypy", "autohooks.plugins.ruff.format", "autohooks.plugins.ruff.check"] From 142fad0b34ca8e0461190dbc3cacc64c9df290d5 Mon Sep 17 00:00:00 2001 From: Vadim Laletin Date: Fri, 28 Feb 2025 12:57:50 +0100 Subject: [PATCH 13/23] Port typescript implementation into python --- python/README.md | 15 ++ python/fpml/core/extract.py | 341 +++++++++++++++++++++++++++++++++++- python/fpml/core/types.py | 3 +- 3 files changed, 352 insertions(+), 7 deletions(-) diff --git a/python/README.md b/python/README.md index b1ce1e3..fcf6749 100644 --- a/python/README.md +++ b/python/README.md @@ -11,3 +11,18 @@ pip install fpml ```python from fpml import resolve_template ``` + +## Development + +In `./python` directory: + +Run in the shell +``` +autohooks activate +``` + +And edit `../.git/hooks/pre-commit` by replacing the first line with +``` +#!/usr/bin/env -S poetry --project=./python run python +``` + diff --git a/python/fpml/core/extract.py b/python/fpml/core/extract.py index 09b8f0f..6f3389e 100644 --- a/python/fpml/core/extract.py +++ b/python/fpml/core/extract.py @@ -1,23 +1,354 @@ -from typing import Any, Optional +import re +from typing import Any, Optional, cast +from fhirpathpy import evaluate # type: ignore +from .constants import root_node_key, undefined +from .exceptions import FPMLValidationError from .types import ( Context, + DictNode, FPOptions, - Model, + Matcher, + MatcherResult, + Node, + Path, Resource, + StrNode, + Transformer, ) +from .utils import flatten, omit_key def resolve_template( resource: Resource, template: Any, context: Optional[Context] = None, - model: Optional[Model] = None, fp_options: Optional[FPOptions] = None, strict: bool = False, ) -> Any: - assert fp_options is None, "fp_options are not supported" assert strict is False, "strict is not supported yet" - return {} \ No newline at end of file + return resolve_template_recur( + [], + resource, + template, + context or {}, + fp_options, + ) + + +def resolve_template_recur( + start_path: Path, + resource: Resource, + template: Any, + context: Context, + fp_options: Optional[FPOptions] = None, +) -> Any: + return iterate_node( + start_path, + {root_node_key: template}, + context or {}, + lambda path, node, context: process_node(path, resource, node, context, fp_options), + ).get(root_node_key, None) + + +def process_node( + path: Path, + resource: Resource, + node: Node, + context: Context, + fp_options: Optional[FPOptions], +) -> tuple[Node, Context]: + if isinstance(node, dict): + new_node, new_context = process_assign_block(path, resource, node, context, fp_options) + + matchers: list[Matcher] = [ + process_context_block, + process_merge_block, + process_for_block, + process_if_block, + ] + + for matcher in matchers: + result = matcher(path, resource, new_node, new_context, fp_options) + if result: + return result["node"], new_context + + return new_node, new_context + + if isinstance(node, str): + return process_template_string(path, resource, node, context, fp_options), context + + return node, context + + +def iterate_node(start_path: Path, node: Node, context: Context, transform: Transformer) -> Node: + if isinstance(node, list): + # Arrays are flattened and null/undefined values are removed here + return flatten( + [ + value + for value in [ + iterate_node( + [*start_path, index], + *transform([*start_path, index], value, context), + transform, + ) + for index, value in enumerate(node) + ] + if value is not None and value is not undefined + ] + ) + if isinstance(node, dict): + # undefined values are removed from dicts, but nulls are preserved + return { + key: value + for key, value in { + key: iterate_node( + [*start_path, key], + *transform([*start_path, key], value, context), + transform, + ) + for key, value in node.items() + }.items() + if value is not undefined + } + + return transform(start_path, node, context)[0] + + +def process_template_string( + path: Path, + resource: Resource, + node: StrNode, + context: Context, + fp_options: Optional[FPOptions], +) -> Any: + template_regexp = re.compile(r"{{\+?\s*([\s\S]+?)\s*\+?}}") + result = node + + for match in template_regexp.finditer(node): + expr = match.group(1) + try: + replacement = evaluate_expression(path, resource, expr, context, fp_options)[0] + except IndexError: + return None if match.group(0).startswith("{{+") else undefined + if match.group(0) == node: + return replacement + result = result.replace(match.group(0), str(replacement)) + + return result + + +def process_context_block( + path: Path, + resource: Resource, + node: DictNode, + context: Context, + fp_options: Optional[FPOptions], +) -> Optional[MatcherResult]: + keys = list(node.keys()) + context_regexp = re.compile(r"{{\s*(.+?)\s*}}") + context_key = next((k for k in keys if context_regexp.match(k)), None) + + if context_key: + matches = context_regexp.match(context_key) + expr = matches.group(1) if matches else "" + + if len(keys) > 1: + raise FPMLValidationError("Context block must be presented as single key", path) + + answers = evaluate_expression(path, resource, expr, context, fp_options) + return { + "node": [ + resolve_template_recur(path, answer, node[context_key], context, fp_options) + for answer in answers + ] + } + + return None + + +def process_for_block( + path: Path, + resource: Resource, + node: DictNode, + context: Context, + fp_options: Optional[FPOptions], +) -> Optional[MatcherResult]: + keys = list(node.keys()) + + for_regexp = re.compile(r"{%\s*for\s+(?:(\w+?)\s*,\s*)?(\w+?)\s+in\s+(.+?)\s*%}") + for_key = next((k for k in keys if for_regexp.match(k)), None) + + if for_key: + matches = for_regexp.match(for_key) + if not matches: + return None + + has_index_key = len(matches.groups()) == 3 # noqa: PLR2004 + index_key = cast(str, matches.group(1)) if has_index_key else None + item_key = cast(str, matches.group(2) if has_index_key else matches.group(1)) + expr = matches.group(3) if has_index_key else matches.group(2) + + if len(keys) > 1: + raise FPMLValidationError("For block must be presented as single key", path) + + answers = evaluate_expression(path, resource, expr, context, fp_options) + + return { + "node": [ + resolve_template_recur( + path, + resource, + node[for_key], + { + **context, + item_key: answer, + **({index_key: index} if index_key else {}), + }, + fp_options, + ) + for index, answer in enumerate(answers) + ] + } + + return None + + +def process_if_block( + path: Path, + resource: Resource, + node: dict[str, Any], + context: Context, + fp_options: Optional[FPOptions], +) -> Optional[MatcherResult]: + keys = list(node.keys()) + + if_regexp = re.compile(r"{%\s*if\s+(.+?)\s*%}") + else_regexp = re.compile(r"{%\s*else\s*%}") + + if_keys = [k for k in keys if if_regexp.match(k)] + if len(if_keys) > 1: + raise FPMLValidationError("If block must be presented once", path) + if_key = if_keys[0] if if_keys else None + + else_keys = [k for k in keys if else_regexp.match(k)] + if len(else_keys) > 1: + raise FPMLValidationError("Else block must be presented once", path) + else_key = else_keys[0] if else_keys else None + + if else_key and not if_key: + raise FPMLValidationError( + "Else block must be presented only when if block is presented", path + ) + + if not if_key: + return None + + matches = if_regexp.match(if_key) + expr = matches.group(1) if matches else "" + + answer = evaluate_expression(path, resource, f"iif({expr}, true, false)", context, fp_options)[ + 0 + ] + + new_node = ( + resolve_template_recur(path, resource, node[if_key], context, fp_options) + if answer + else ( + resolve_template_recur(path, resource, node[else_key], context, fp_options) + if else_key + else undefined + ) + ) + + is_merge_behavior = len(keys) != (2 if else_key else 1) + if is_merge_behavior: + if not isinstance(new_node, dict) and new_node is not None and new_node is not undefined: + raise FPMLValidationError( + "If/else block must return object for implicit merge into existing node", + path, + ) + + return { + "node": { + **omit_key(omit_key(node, if_key), else_key), + **(new_node if isinstance(new_node, dict) else {}), + } + } + + return {"node": new_node} + + +def process_merge_block( + path: Path, + resource: Resource, + node: DictNode, + context: Context, + fp_options: Optional[FPOptions], +) -> Optional[MatcherResult]: + merge_key = next((k for k in node if re.match(r"{%\s*merge\s*%}", k)), None) + if merge_key: + merged_node = omit_key(node, merge_key) + values = node[merge_key] if isinstance(node[merge_key], list) else [node[merge_key]] + for value in values: + result = resolve_template_recur(path, resource, value, context, fp_options) + if not isinstance(result, dict) and result is not None and result is not undefined: + raise FPMLValidationError("Merge block must contain object", path) + if result is not undefined and result is not None: + merged_node.update(result) + return {"node": merged_node} + return None + + +def process_assign_block( + path: Path, + resource: Resource, + node: DictNode, + context: Context, + fp_options: Optional[FPOptions], +) -> tuple[DictNode, Context]: + extended_context = context.copy() + assign_key = next((k for k in node if re.match(r"{%\s*assign\s*%}", k)), None) + if assign_key: + if isinstance(node[assign_key], list): + for obj in node[assign_key]: + if len(obj) != 1: + raise FPMLValidationError( + "Assign block must accept only one key per object", path + ) + extended_context.update( + resolve_template_recur(path, resource, obj, extended_context, fp_options) + ) + elif isinstance(node[assign_key], dict) and len(node[assign_key]) == 1: + extended_context.update( + resolve_template_recur( + path, + resource, + node[assign_key], + extended_context, + fp_options, + ) + ) + else: + raise FPMLValidationError("Assign block must accept array or object", path) + return omit_key(node, assign_key), extended_context + return node, context + + +def evaluate_expression( + path: Path, + resource: Resource, + expression: str, + context: Context, + fp_options: Optional[FPOptions] = None, +) -> list[Any]: + fp_options_with_default = cast(FPOptions, fp_options or {}) + model = fp_options_with_default.get("model") + + try: + return evaluate(resource, expression, context, model) + except Exception as exc: + raise FPMLValidationError(f"Cannot evaluate '{expression}': {exc}", path) from exc diff --git a/python/fpml/core/types.py b/python/fpml/core/types.py index e58c2bf..4732b6f 100644 --- a/python/fpml/core/types.py +++ b/python/fpml/core/types.py @@ -17,7 +17,7 @@ class Model(TypedDict): class FPOptions(TypedDict): - pass + model: Optional[Model] class MatcherResult(TypedDict): @@ -30,7 +30,6 @@ class MatcherResult(TypedDict): Resource, DictNode, Context, - Optional[Model], Optional[FPOptions], ], Optional[MatcherResult], From e451f9fc9921c0c4390fbdafa1c7195848c80cb3 Mon Sep 17 00:00:00 2001 From: Vadim Laletin Date: Fri, 28 Feb 2025 16:59:12 +0100 Subject: [PATCH 14/23] Re-write tests by moving data to yaml file fixtures --- .../complex-example.aidbox.context.yaml | 107 ++++ .../complex-example.aidbox.result.yaml | 213 ++++++++ ...l => complex-example.aidbox.template.yaml} | 33 +- .../complex-example.fhir.context.yaml | 93 ++++ .../__data__/complex-example.fhir.result.yaml | 195 ++++++++ ...aml => complex-example.fhir.template.yaml} | 52 +- .../src/utils/complex-example.aidbox.spec.ts | 455 +----------------- .../src/utils/complex-example.fhir.spec.ts | 400 +-------------- 8 files changed, 680 insertions(+), 868 deletions(-) create mode 100644 tests/__data__/complex-example.aidbox.context.yaml create mode 100644 tests/__data__/complex-example.aidbox.result.yaml rename tests/__data__/{complex-example.aidbox.yaml => complex-example.aidbox.template.yaml} (83%) create mode 100644 tests/__data__/complex-example.fhir.context.yaml create mode 100644 tests/__data__/complex-example.fhir.result.yaml rename tests/__data__/{complex-example.fhir.yaml => complex-example.fhir.template.yaml} (74%) diff --git a/tests/__data__/complex-example.aidbox.context.yaml b/tests/__data__/complex-example.aidbox.context.yaml new file mode 100644 index 0000000..cfebc4f --- /dev/null +++ b/tests/__data__/complex-example.aidbox.context.yaml @@ -0,0 +1,107 @@ +QuestionnaireResponse: + resourceType: QuestionnaireResponse + id: qrid + authored: '2024-01-01' + item: + - linkid: root + item: + - linkId: WEIGHT + answer: + value: + decimal: 100 + - linkId: HEIGHT + answer: + value: + decimal: 190 + - linkId: MEDCOND1 + answer: + - value: + Coding: + system: 'urn:raw' + code: hypertension + display: Hypertension + - value: + Coding: + system: 'urn:raw' + code: fatty-liver + display: Fatty Liver + - linkId: MEDCOND2 + answer: + - value: + Coding: + system: 'urn:raw' + code: asthma + display: Asthma + +Provenance: + - resourceType: Provenance + id: prov-hypertension + target: + - resourceType: Condition + id: cond-hypertension + recorded: '2024-01-01' + agent: + - who: + resourceType: Organization + id: orgid + entity: + - role: source + what: + resourceType: QuestionnaireResponse + id: qrid + - resourceType: Provenance + id: prov-flu + target: + - resourceType: Condition + id: cond1 + recorded: '2024-01-01' + agent: + - who: + resourceType: Organization + id: orgid + entity: + - role: source + what: + resourceType: QuestionnaireResponse + id: qrid + +Observation: [] +Condition: + - resourceType: Condition + id: cond-flu + subject: + resourceType: Patient + id: pid + recordedDate: '2024-01-01' + code: + coding: + - system: 'urn:raw' + code: Flu + display: Flu + text: Flu + category: + - coding: + - code: medicalHistory + display: Medical history + - resourceType: Condition + id: cond1 + subject: + resourceType: Patient + id: pid + recordedDate: '2024-01-01' + code: + coding: + - system: 'urn:raw' + code: hypertension + display: Hypertension + text: Hypertension + category: + - coding: + - code: medicalHistory + display: Medical history +Organization: + resourceType: Organization + id: orgid +Patient: + resourceType: Patient + id: pid diff --git a/tests/__data__/complex-example.aidbox.result.yaml b/tests/__data__/complex-example.aidbox.result.yaml new file mode 100644 index 0000000..5483027 --- /dev/null +++ b/tests/__data__/complex-example.aidbox.result.yaml @@ -0,0 +1,213 @@ +body: + resourceType: Bundle + type: transaction + entry: + - fullUrl: 'urn:uuid:observation-weight' + request: + url: >- + /Observation?patient=pid&category=vital-signs&code=http://loinc.org|29463-7 + method: POST + resource: + resourceType: Observation + subject: + resourceType: Patient + id: pid + status: final + effective: + dateTime: '2024-01-01' + category: + - coding: + - system: 'http://terminology.hl7.org/CodeSystem/observation-category' + code: vital-signs + code: + coding: + - system: 'http://loinc.org' + code: 29463-7 + display: Body Weight + value: + Quantity: + value: 100 + unit: kg + system: 'http://unitsofmeasure.org' + code: kg + - fullUrl: 'urn:uuid:observation-height' + request: + url: >- + /Observation?patient=pid&category=vital-signs&code=http://loinc.org|8302-2 + method: POST + resource: + resourceType: Observation + subject: + resourceType: Patient + id: pid + status: final + effective: + dateTime: '2024-01-01' + category: + - coding: + - system: 'http://terminology.hl7.org/CodeSystem/observation-category' + code: vital-signs + code: + coding: + - system: 'http://loinc.org' + code: 8302-2 + display: Body Height + value: + Quantity: + value: 190 + unit: kg + system: 'http://unitsofmeasure.org' + code: kg + - fullUrl: 'urn:uuid:condition-medical-history-0' + request: + url: Condition/cond1 + method: PUT + resource: + resourceType: Condition + id: cond1 + subject: + resourceType: Patient + id: pid + recordedDate: '2024-01-01' + code: + coding: + - system: 'urn:raw' + code: hypertension + display: Hypertension + text: Hypertension + category: + - coding: + - code: medicalHistory + display: Medical history + - fullUrl: 'urn:uuid:condition-medical-history-1' + request: + url: >- + /Condition?category=medicalHistory&code=urn:raw|fatty-liver&patient=pid + method: POST + resource: + resourceType: Condition + subject: + resourceType: Patient + id: pid + recordedDate: '2024-01-01' + code: + coding: + - system: 'urn:raw' + code: fatty-liver + display: Fatty Liver + text: Fatty Liver + category: + - coding: + - code: medicalHistory + display: Medical history + - fullUrl: 'urn:uuid:condition-medical-history-2' + request: + url: '/Condition?category=medicalHistory&code=urn:raw|asthma&patient=pid' + method: POST + resource: + resourceType: Condition + subject: + resourceType: Patient + id: pid + recordedDate: '2024-01-01' + code: + coding: + - system: 'urn:raw' + code: asthma + display: Asthma + text: Asthma + category: + - coding: + - code: medicalHistory + display: Medical history + - request: + url: /Provenance + method: POST + resource: + resourceType: Provenance + target: + - uri: 'urn:uuid:observation-weight' + recorded: '2024-01-01' + agent: + - who: + resourceType: Organization + id: orgid + entity: + - role: source + what: + resourceType: QuestionnaireResponse + id: qrid + - request: + url: /Provenance + method: POST + resource: + resourceType: Provenance + target: + - uri: 'urn:uuid:observation-height' + recorded: '2024-01-01' + agent: + - who: + resourceType: Organization + id: orgid + entity: + - role: source + what: + resourceType: QuestionnaireResponse + id: qrid + - request: + url: /Provenance + method: POST + resource: + resourceType: Provenance + target: + - uri: 'urn:uuid:condition-medical-history-0' + recorded: '2024-01-01' + agent: + - who: + resourceType: Organization + id: orgid + entity: + - role: source + what: + resourceType: QuestionnaireResponse + id: qrid + - request: + url: /Provenance + method: POST + resource: + resourceType: Provenance + target: + - uri: 'urn:uuid:condition-medical-history-1' + recorded: '2024-01-01' + agent: + - who: + resourceType: Organization + id: orgid + entity: + - role: source + what: + resourceType: QuestionnaireResponse + id: qrid + - request: + url: /Provenance + method: POST + resource: + resourceType: Provenance + target: + - uri: 'urn:uuid:condition-medical-history-2' + recorded: '2024-01-01' + agent: + - who: + resourceType: Organization + id: orgid + entity: + - role: source + what: + resourceType: QuestionnaireResponse + id: qrid + - request: + url: /Provenance/prov-hypertension + method: DELETE + - request: + url: /Condition/cond-hypertension + method: DELETE diff --git a/tests/__data__/complex-example.aidbox.yaml b/tests/__data__/complex-example.aidbox.template.yaml similarity index 83% rename from tests/__data__/complex-example.aidbox.yaml rename to tests/__data__/complex-example.aidbox.template.yaml index c21bd4f..6f5ffc3 100644 --- a/tests/__data__/complex-example.aidbox.yaml +++ b/tests/__data__/complex-example.aidbox.template.yaml @@ -2,13 +2,19 @@ body: "{% assign %}": - patientId: "{{ %Patient.id }}" - recordedDate: "{{ %QuestionnaireResponse.authored }}" + - provenanceTargetIds: + "{% for id in %Provenance.target.id %}": "{{ %id }}" + - weight: "{{ repeat(item).where(linkId='WEIGHT').answer.value.children() }}" + - height: "{{ repeat(item).where(linkId='HEIGHT').answer.value.children() }}" + - medConditions: + "{% for answer in repeat(item).where(linkId in 'MEDCOND1' | 'MEDCOND2').answer.value.children() %}": "{{ %answer }}" - observationEntries: - - "{% if answers('WEIGHT').exists() and answers('HEIGHT').exists() %}": + - "{% if %weight.exists() and %height.exists() %}": "{% assign %}": - observationId: >- {{ %Observation.where( - id in %Provenance.target.id and + id in %provenanceTargetIds and category.coding.code='vital-signs' and resourceType='Observation' and code.coding.code='29463-7' @@ -43,21 +49,18 @@ body: value: Quantity: value: - "{% assign %}": - - rawHeight: "{{ answers('HEIGHT') }}" - - rawWeight: "{{ answers('WEIGHT') }}" - "{% if %rawHeight < 90 %}": "{{ %rawWeight / 2.205 }}" - "{% else %}": "{{ %rawWeight }}" + "{% if %height < 90 %}": "{{ %weight / 2.205 }}" + "{% else %}": "{{ %weight }}" unit: kg system: "http://unitsofmeasure.org" code: kg - - "{% if answers('HEIGHT').exists() %}": + - "{% if %height.exists() %}": "{% assign %}": - observationId: >- {{ %Observation.where( - id in %Provenance.target.id and + id in %provenanceTargetIds and category.coding.code='vital-signs' and resourceType='Observation' and code.coding.code='29463-7' @@ -92,22 +95,20 @@ body: value: Quantity: value: - "{% assign %}": - - rawHeight: "{{ answers('HEIGHT') }}" # 90 inch ~ 230cm - "{% if %rawHeight < 90 %}": "{{ %rawHeight * 2.54 }}" # inches to cm - "{% else %}": "{{ %rawHeight }}" # cm + "{% if %height < 90 %}": "{{ %height * 2.54 }}" # inches to cm + "{% else %}": "{{ %height }}" # cm unit: kg system: "http://unitsofmeasure.org" code: kg - conditionEntries: - "{% for index, coding in answers('MEDCOND1') | answers('MEDCOND2') %}": + "{% for index, coding in %medConditions %}": "{% assign %}": - conditionId: >- {{ %Condition.where(resourceType='Condition').where( - id in %Provenance.target.id and + id in %provenanceTargetIds and category.coding.code='medicalHistory' and code.coding.system=%coding.system and code.coding.code=%coding.code @@ -161,7 +162,7 @@ body: - "{% for entry in %observationEntries | %conditionEntries | %provenanceEntries %}": "{{ %entry }}" - "{% assign %}": - resourceIdsToDelete: - "{% for id in %Provenance.target.id.exclude((%observationEntries | %conditionEntries).resource.id) %}": "{{ %id }}" + "{% for id in %provenanceTargetIds.exclude((%observationEntries | %conditionEntries).resource.id) %}": "{{ %id }}" "{% for provenance in %Provenance.where(target.id in %resourceIdsToDelete) %}": - request: url: "/Provenance/{{ %provenance.id }}" diff --git a/tests/__data__/complex-example.fhir.context.yaml b/tests/__data__/complex-example.fhir.context.yaml new file mode 100644 index 0000000..7cd631f --- /dev/null +++ b/tests/__data__/complex-example.fhir.context.yaml @@ -0,0 +1,93 @@ +QuestionnaireResponse: + resourceType: QuestionnaireResponse + id: qrid + authored: '2024-01-01' + item: + - linkid: root + item: + - linkId: WEIGHT + answer: + valueDecimal: 100 + - linkId: HEIGHT + answer: + valueDecimal: 190 + - linkId: MEDCOND1 + answer: + - valueCoding: + system: 'urn:raw' + code: hypertension + display: Hypertension + - valueCoding: + system: 'urn:raw' + code: fatty-liver + display: Fatty Liver + - linkId: MEDCOND2 + answer: + - valueCoding: + system: 'urn:raw' + code: asthma + display: Asthma + +Provenance: + - resourceType: Provenance + id: prov-hypertension + target: + - reference: Condition/cond-hypertension + recorded: '2024-01-01' + agent: + - who: + reference: Organization/orgid + entity: + - role: source + what: + reference: QuestionnaireResponse/qrid + - resourceType: Provenance + id: prov-flu + target: + - reference: Condition/cond1 + recorded: '2024-01-01' + agent: + - who: Organization/orgid + entity: + - role: source + what: + reference: QuestionnaireResponse/qrid +Observation: [] +Condition: + - resourceType: Condition + id: cond-flu + subject: + reference: Patient/pid + recordedDate: '2024-01-01' + code: + coding: + - system: 'urn:raw' + code: Flu + display: Flu + text: Flu + category: + - coding: + - code: medicalHistory + display: Medical history + - resourceType: Condition + id: cond1 + subject: + reference: Patient/pid + recordedDate: '2024-01-01' + code: + coding: + - system: 'urn:raw' + code: hypertension + display: Hypertension + text: Hypertension + category: + + - coding: + - code: medicalHistory + display: Medical history +Organization: + resourceType: Organization + id: orgid +Patient: + resourceType: Patient + id: pid diff --git a/tests/__data__/complex-example.fhir.result.yaml b/tests/__data__/complex-example.fhir.result.yaml new file mode 100644 index 0000000..3bde852 --- /dev/null +++ b/tests/__data__/complex-example.fhir.result.yaml @@ -0,0 +1,195 @@ +body: + resourceType: Bundle + type: transaction + entry: + - fullUrl: 'urn:uuid:observation-weight' + request: + url: >- + /Observation?patient=Patient/pid&category=vital-signs&code=http://loinc.org|29463-7 + method: POST + resource: + resourceType: Observation + subject: + reference: Patient/pid + status: final + effectiveDateTime: '2024-01-01' + category: + - coding: + - system: 'http://terminology.hl7.org/CodeSystem/observation-category' + code: vital-signs + code: + coding: + - system: 'http://loinc.org' + code: 29463-7 + display: Body Weight + valueQuantity: + value: 100 + unit: kg + system: 'http://unitsofmeasure.org' + code: kg + - fullUrl: 'urn:uuid:observation-height' + request: + url: >- + /Observation?patient=Patient/pid&category=vital-signs&code=http://loinc.org|8302-2 + method: POST + resource: + resourceType: Observation + subject: + reference: Patient/pid + status: final + effectiveDateTime: '2024-01-01' + category: + - coding: + - system: 'http://terminology.hl7.org/CodeSystem/observation-category' + code: vital-signs + code: + coding: + - system: 'http://loinc.org' + code: 8302-2 + display: Body Height + valueQuantity: + value: 190 + unit: kg + system: 'http://unitsofmeasure.org' + code: kg + - fullUrl: 'urn:uuid:condition-medical-history-0' + request: + url: Condition/cond1 + method: PUT + resource: + resourceType: Condition + id: cond1 + subject: + reference: Patient/pid + recordedDate: '2024-01-01' + code: + coding: + - system: 'urn:raw' + code: hypertension + display: Hypertension + text: Hypertension + category: + - coding: + - code: medicalHistory + display: Medical history + - fullUrl: 'urn:uuid:condition-medical-history-1' + request: + url: >- + /Condition?category=medicalHistory&code=urn:raw|fatty-liver&patient=Patient/pid + method: POST + resource: + resourceType: Condition + subject: + reference: Patient/pid + recordedDate: '2024-01-01' + code: + coding: + - system: 'urn:raw' + code: fatty-liver + display: Fatty Liver + text: Fatty Liver + category: + - coding: + - code: medicalHistory + display: Medical history + - fullUrl: 'urn:uuid:condition-medical-history-2' + request: + url: >- + /Condition?category=medicalHistory&code=urn:raw|asthma&patient=Patient/pid + method: POST + resource: + resourceType: Condition + subject: + reference: Patient/pid + recordedDate: '2024-01-01' + code: + coding: + - system: 'urn:raw' + code: asthma + display: Asthma + text: Asthma + category: + - coding: + - code: medicalHistory + display: Medical history + - request: + url: /Provenance + method: POST + resource: + resourceType: Provenance + target: + - uri: 'urn:uuid:observation-weight' + recorded: '2024-01-01' + agent: + - who: + reference: Organization/orgid + entity: + - role: source + what: + reference: QuestionnaireResponse/qrid + - request: + url: /Provenance + method: POST + resource: + resourceType: Provenance + target: + - uri: 'urn:uuid:observation-height' + recorded: '2024-01-01' + agent: + - who: + reference: Organization/orgid + entity: + - role: source + what: + reference: QuestionnaireResponse/qrid + - request: + url: /Provenance + method: POST + resource: + resourceType: Provenance + target: + - uri: 'urn:uuid:condition-medical-history-0' + recorded: '2024-01-01' + agent: + - who: + reference: Organization/orgid + entity: + - role: source + what: + reference: QuestionnaireResponse/qrid + - request: + url: /Provenance + method: POST + resource: + resourceType: Provenance + target: + - uri: 'urn:uuid:condition-medical-history-1' + recorded: '2024-01-01' + agent: + - who: + reference: Organization/orgid + entity: + - role: source + what: + reference: QuestionnaireResponse/qrid + - request: + url: /Provenance + method: POST + resource: + resourceType: Provenance + target: + - uri: 'urn:uuid:condition-medical-history-2' + recorded: '2024-01-01' + agent: + - who: + reference: Organization/orgid + entity: + - role: source + what: + reference: QuestionnaireResponse/qrid + - request: + url: /Provenance/prov-hypertension + method: DELETE + - request: + url: /Condition/cond-hypertension + method: DELETE diff --git a/tests/__data__/complex-example.fhir.yaml b/tests/__data__/complex-example.fhir.template.yaml similarity index 74% rename from tests/__data__/complex-example.fhir.yaml rename to tests/__data__/complex-example.fhir.template.yaml index 51e7a25..562c79c 100644 --- a/tests/__data__/complex-example.fhir.yaml +++ b/tests/__data__/complex-example.fhir.template.yaml @@ -2,13 +2,19 @@ body: "{% assign %}": - patientRef: "Patient/{{ %Patient.id }}" - recordedDate: "{{ %QuestionnaireResponse.authored }}" + - provenanceTargetIds: + "{% for id in %Provenance.target.select(reference.split('/').last()) %}": "{{ %id }}" + - weight: "{{ repeat(item).where(linkId='WEIGHT').answer.value }}" + - height: "{{ repeat(item).where(linkId='HEIGHT').answer.value }}" + - medConditions: + "{% for answer in repeat(item).where(linkId in 'MEDCOND1' | 'MEDCOND2').answer.value %}": "{{ %answer }}" - observationEntries: - - "{% if answers('WEIGHT').exists() and answers('HEIGHT').exists() %}": + - "{% if %weight and %height %}": "{% assign %}": - observationId: >- {{ %Observation.where( - id in %Provenance.target.id and + id in %provenanceTargetIds and category.coding.code='vital-signs' and resourceType='Observation' and code.coding.code='29463-7' @@ -25,7 +31,8 @@ body: resource: resourceType: Observation id: "{{ %observationId }}" - subject: "{{ %patientRef }}" + subject: + reference: "{{ %patientRef }}" status: final effectiveDateTime: "{{ %recordedDate }}" category: @@ -39,21 +46,18 @@ body: display: Body Weight valueQuantity: value: - "{% assign %}": - - rawHeight: "{{ answers('HEIGHT') }}" - - rawWeight: "{{ answers('WEIGHT') }}" - "{% if %rawHeight < 90 %}": "{{ %rawWeight / 2.205 }}" - "{% else %}": "{{ %rawWeight }}" + "{% if %height < 90 %}": "{{ %weight / 2.205 }}" + "{% else %}": "{{ %weight }}" unit: kg system: "http://unitsofmeasure.org" code: kg - - "{% if answers('HEIGHT').exists() %}": + - "{% if %height.exists() %}": "{% assign %}": - observationId: >- {{ %Observation.where( - id in %Provenance.target.id and + id in %provenanceTargetIds and category.coding.code='vital-signs' and resourceType='Observation' and code.coding.code='29463-7' @@ -70,7 +74,8 @@ body: resource: resourceType: Observation id: "{{ %observationId }}" - subject: "{{ %patientRef }}" + subject: + reference: "{{ %patientRef }}" status: final effectiveDateTime: "{{ %recordedDate }}" category: @@ -84,22 +89,20 @@ body: display: Body Height valueQuantity: value: - "{% assign %}": - - rawHeight: "{{ answers('HEIGHT') }}" # 90 inch ~ 230cm - "{% if %rawHeight < 90 %}": "{{ %rawHeight * 2.54 }}" # inches to cm - "{% else %}": "{{ %rawHeight }}" # cm + "{% if %height < 90 %}": "{{ %height * 2.54 }}" # inches to cm + "{% else %}": "{{ %height }}" # cm unit: kg system: "http://unitsofmeasure.org" code: kg - conditionEntries: - "{% for index, coding in answers('MEDCOND1') | answers('MEDCOND2') %}": + "{% for index, coding in %medConditions %}": "{% assign %}": - conditionId: >- {{ %Condition.where( - id in %Provenance.target.id and + id in %provenanceTargetIds and category.coding.code='medicalHistory' and code.coding.system=%coding.system and code.coding.code=%coding.code @@ -116,7 +119,8 @@ body: resource: resourceType: Condition id: "{{ %conditionId }}" - subject: "{{ %patientRef }}" + subject: + reference: "{{ %patientRef }}" recordedDate: "{{ %recordedDate }}" code: coding: @@ -137,21 +141,23 @@ body: - uri: "{{ %entry.fullUrl }}" recorded: "{{ %recordedDate }}" agent: - - who: "Organization/{{ %Organization.id }}" + - who: + reference: "Organization/{{ %Organization.id }}" entity: - role: source - what: "QuestionnaireResponse/{{ %QuestionnaireResponse.id }}" + what: + reference: "QuestionnaireResponse/{{ %QuestionnaireResponse.id }}" resourceType: Bundle type: transaction entry: - "{% for entry in %observationEntries | %conditionEntries | %provenanceEntries %}": "{{ %entry }}" - "{% assign %}": - resourceIdsToDelete: - "{% for id in %Provenance.target.id.exclude((%observationEntries | %conditionEntries).resource.id) %}": "{{ %id }}" - "{% for provenance in %Provenance.where(target.id in %resourceIdsToDelete) %}": + "{% for id in %provenanceTargetIds.exclude((%observationEntries | %conditionEntries).resource.id) %}": "{{ %id }}" + "{% for provenance in %Provenance.where(target.reference.split('/').last() in %resourceIdsToDelete) %}": - request: url: "/Provenance/{{ %provenance.id }}" method: DELETE - request: - url: "/{{ %provenance.target.resourceType }}/{{ %provenance.target.id }}" + url: "/{{ %provenance.target.reference }}" method: DELETE diff --git a/ts/server/src/utils/complex-example.aidbox.spec.ts b/ts/server/src/utils/complex-example.aidbox.spec.ts index 14b28d8..199e32e 100644 --- a/ts/server/src/utils/complex-example.aidbox.spec.ts +++ b/ts/server/src/utils/complex-example.aidbox.spec.ts @@ -1,455 +1,26 @@ import { resolveTemplate } from './extract'; -import * as fhirpath from 'fhirpath'; import * as yaml from 'js-yaml'; import * as fs from 'fs'; import * as path from 'path'; const template = yaml.load( - fs.readFileSync(path.join(__dirname, './__data__/complex-example.aidbox.yaml'), 'utf8'), + fs.readFileSync(path.join(__dirname, './__data__/complex-example.aidbox.template.yaml'), 'utf8'), +); +const result = yaml.load( + fs.readFileSync(path.join(__dirname, './__data__/complex-example.aidbox.result.yaml'), 'utf8'), +); +const context = yaml.load( + fs.readFileSync(path.join(__dirname, './__data__/complex-example.aidbox.context.yaml'), 'utf8'), ); -const result = { - body: { - resourceType: 'Bundle', - type: 'transaction', - entry: [ - { - fullUrl: 'urn:uuid:observation-weight', - request: { - url: '/Observation?patient=pid&category=vital-signs&code=http://loinc.org|29463-7', - method: 'POST', - }, - resource: { - resourceType: 'Observation', - id: undefined, - subject: { resourceType: 'Patient', id: 'pid' }, - status: 'final', - effective: { dateTime: '2024-01-01' }, - category: [ - { - coding: [ - { - system: 'http://terminology.hl7.org/CodeSystem/observation-category', - code: 'vital-signs', - }, - ], - }, - ], - code: { - coding: [ - { system: 'http://loinc.org', code: '29463-7', display: 'Body Weight' }, - ], - }, - value: { - Quantity: { - value: 100, - unit: 'kg', - system: 'http://unitsofmeasure.org', - code: 'kg', - }, - }, - }, - }, - { - fullUrl: 'urn:uuid:observation-height', - request: { - url: '/Observation?patient=pid&category=vital-signs&code=http://loinc.org|8302-2', - method: 'POST', - }, - resource: { - resourceType: 'Observation', - id: undefined, - subject: { resourceType: 'Patient', id: 'pid' }, - status: 'final', - effective: { dateTime: '2024-01-01' }, - category: [ - { - coding: [ - { - system: 'http://terminology.hl7.org/CodeSystem/observation-category', - code: 'vital-signs', - }, - ], - }, - ], - code: { - coding: [ - { system: 'http://loinc.org', code: '8302-2', display: 'Body Height' }, - ], - }, - value: { - Quantity: { - value: 190, - unit: 'kg', - system: 'http://unitsofmeasure.org', - code: 'kg', - }, - }, - }, - }, - { - fullUrl: 'urn:uuid:condition-medical-history-0', - request: { url: 'Condition/cond1', method: 'PUT' }, - resource: { - resourceType: 'Condition', - id: 'cond1', - subject: { resourceType: 'Patient', id: 'pid' }, - recordedDate: '2024-01-01', - code: { - coding: [ - { system: 'urn:raw', code: 'hypertension', display: 'Hypertension' }, - ], - text: 'Hypertension', - }, - category: [ - { coding: [{ code: 'medicalHistory', display: 'Medical history' }] }, - ], - }, - }, - { - fullUrl: 'urn:uuid:condition-medical-history-1', - request: { - url: '/Condition?category=medicalHistory&code=urn:raw|fatty-liver&patient=pid', - method: 'POST', - }, - resource: { - resourceType: 'Condition', - id: undefined, - subject: { resourceType: 'Patient', id: 'pid' }, - recordedDate: '2024-01-01', - code: { - coding: [ - { system: 'urn:raw', code: 'fatty-liver', display: 'Fatty Liver' }, - ], - text: 'Fatty Liver', - }, - category: [ - { coding: [{ code: 'medicalHistory', display: 'Medical history' }] }, - ], - }, - }, - { - fullUrl: 'urn:uuid:condition-medical-history-2', - request: { - url: '/Condition?category=medicalHistory&code=urn:raw|asthma&patient=pid', - method: 'POST', - }, - resource: { - resourceType: 'Condition', - id: undefined, - subject: { resourceType: 'Patient', id: 'pid' }, - recordedDate: '2024-01-01', - code: { - coding: [{ system: 'urn:raw', code: 'asthma', display: 'Asthma' }], - text: 'Asthma', - }, - category: [ - { coding: [{ code: 'medicalHistory', display: 'Medical history' }] }, - ], - }, - }, - { - request: { url: '/Provenance', method: 'POST' }, - resource: { - resourceType: 'Provenance', - target: [{ uri: 'urn:uuid:observation-weight' }], - recorded: '2024-01-01', - agent: [{ who: { resourceType: 'Organization', id: 'orgid' } }], - entity: [ - { - role: 'source', - what: { resourceType: 'QuestionnaireResponse', id: 'qrid' }, - }, - ], - }, - }, - { - request: { url: '/Provenance', method: 'POST' }, - resource: { - resourceType: 'Provenance', - target: [{ uri: 'urn:uuid:observation-height' }], - recorded: '2024-01-01', - agent: [{ who: { resourceType: 'Organization', id: 'orgid' } }], - entity: [ - { - role: 'source', - what: { resourceType: 'QuestionnaireResponse', id: 'qrid' }, - }, - ], - }, - }, - { - request: { url: '/Provenance', method: 'POST' }, - resource: { - resourceType: 'Provenance', - target: [{ uri: 'urn:uuid:condition-medical-history-0' }], - recorded: '2024-01-01', - agent: [{ who: { resourceType: 'Organization', id: 'orgid' } }], - entity: [ - { - role: 'source', - what: { resourceType: 'QuestionnaireResponse', id: 'qrid' }, - }, - ], - }, - }, - { - request: { url: '/Provenance', method: 'POST' }, - resource: { - resourceType: 'Provenance', - target: [{ uri: 'urn:uuid:condition-medical-history-1' }], - recorded: '2024-01-01', - agent: [{ who: { resourceType: 'Organization', id: 'orgid' } }], - entity: [ - { - role: 'source', - what: { resourceType: 'QuestionnaireResponse', id: 'qrid' }, - }, - ], - }, - }, - { - request: { url: '/Provenance', method: 'POST' }, - resource: { - resourceType: 'Provenance', - target: [{ uri: 'urn:uuid:condition-medical-history-2' }], - recorded: '2024-01-01', - agent: [{ who: { resourceType: 'Organization', id: 'orgid' } }], - entity: [ - { - role: 'source', - what: { resourceType: 'QuestionnaireResponse', id: 'qrid' }, - }, - ], - }, - }, - { request: { url: '/Provenance/prov-hypertension', method: 'DELETE' } }, - { request: { url: '/Condition/cond-hypertension', method: 'DELETE' } }, - ], - }, -}; - -const qr = { - resourceType: 'QuestionnaireResponse', - id: 'qrid', - authored: '2024-01-01', - item: [ - { - linkid: 'root', - item: [ - { - linkId: 'WEIGHT', - answer: { - value: { - decimal: 100, - }, - }, - }, - { - linkId: 'HEIGHT', - answer: { - value: { - decimal: 190, - }, - }, - }, - { - linkId: 'MEDCOND1', - answer: [ - { - value: { - Coding: { - system: 'urn:raw', - code: 'hypertension', - display: 'Hypertension', - }, - }, - }, - { - value: { - Coding: { - system: 'urn:raw', - code: 'fatty-liver', - display: 'Fatty Liver', - }, - }, - }, - ], - }, - { - linkId: 'MEDCOND2', - answer: [ - { - value: { - Coding: { - system: 'urn:raw', - code: 'asthma', - display: 'Asthma', - }, - }, - }, - ], - }, - ], - }, - ], -}; -const provenances = [ - { - resourceType: 'Provenance', - id: 'prov-hypertension', - target: [ - { - resourceType: 'Condition', - id: 'cond-hypertension', - }, - ], - recorded: '2024-01-01', - agent: [ - { - who: { - resourceType: 'Organization', - id: 'orgid', - }, - }, - ], - entity: [ - { - role: 'source', - what: { - resourceType: 'QuestionnaireResponse', - id: 'qrid', - }, - }, - ], - }, - { - resourceType: 'Provenance', - id: 'prov-flu', - target: [ - { - resourceType: 'Condition', - id: 'cond1', - }, - ], - recorded: '2024-01-01', - agent: [ - { - who: { - resourceType: 'Organization', - id: 'orgid', - }, - }, - ], - entity: [ - { - role: 'source', - what: { - resourceType: 'QuestionnaireResponse', - id: 'qrid', - }, - }, - ], - }, -]; -const organization = { resourceType: 'Organization', id: 'orgid' }; -const patient = { - resourceType: 'Patient', - id: 'pid', -}; -const observations = []; -const conditions = [ - { - resourceType: 'Condition', - id: 'cond1', - subject: { - resourceType: 'Patient', - id: 'pid', - }, - recordedDate: '2024-01-01', - code: { - coding: [ - { - system: 'urn:raw', - code: 'hypertension', - display: 'Hypertension', - }, - ], - text: 'Hypertension', - }, - category: [ - { - coding: [ - { - code: 'medicalHistory', - display: 'Medical history', - }, - ], - }, - ], - }, - { - resourceType: 'Condition', - id: 'cond-flu', - subject: { - resourceType: 'Patient', - id: 'pid', - }, - recordedDate: '2024-01-01', - code: { - coding: [ - { - system: 'urn:raw', - code: 'Flu', - display: 'Flu', - }, - ], - text: 'Flu', - }, - category: [ - { - coding: [ - { - code: 'medicalHistory', - display: 'Medical history', - }, - ], - }, - ], - }, -]; - -const userInvocationTable: UserInvocationTable = { - answers: { - fn: (inputs, linkId: string) => { - return fhirpath.evaluate( - inputs, - `repeat(item).where(linkId='${linkId}').answer.value.children()`, - ); - }, - arity: { 0: [], 1: ['String'] }, - }, - // Get rid of toString once it's fixed https://github.com/HL7/fhirpath.js/issues/156 - toString: { - fn: (inputs) => fhirpath.evaluate({ x: inputs }, 'x.toString()'), - arity: { 0: [] }, - }, -}; -test('Test real example', () => { +test('Test real example (aidbox)', () => { + // Re-wrap to get rid of undefined (reconsider it after #17) expect( - resolveTemplate( - qr as any, + JSON.parse(JSON.stringify(resolveTemplate( + (context as any).QuestionnaireResponse, template, - { - QuestionnaireResponse: qr, - Provenance: provenances, - Organization: organization, - Observation: observations, - Condition: conditions, - Patient: patient, - }, + context, null, - { userInvocationTable }, - ), + ))), ).toStrictEqual(result); }); diff --git a/ts/server/src/utils/complex-example.fhir.spec.ts b/ts/server/src/utils/complex-example.fhir.spec.ts index 1a95aac..b135cce 100644 --- a/ts/server/src/utils/complex-example.fhir.spec.ts +++ b/ts/server/src/utils/complex-example.fhir.spec.ts @@ -1,401 +1,27 @@ import { resolveTemplate } from './extract'; -import * as fhirpath from 'fhirpath'; import * as fhirpath_r4_model from 'fhirpath/fhir-context/r4'; import * as yaml from 'js-yaml'; import * as fs from 'fs'; import * as path from 'path'; const template = yaml.load( - fs.readFileSync(path.join(__dirname, './__data__/complex-example.fhir.yaml'), 'utf8'), + fs.readFileSync(path.join(__dirname, './__data__/complex-example.fhir.template.yaml'), 'utf8'), +); +const result = yaml.load( + fs.readFileSync(path.join(__dirname, './__data__/complex-example.fhir.result.yaml'), 'utf8'), +); +const context = yaml.load( + fs.readFileSync(path.join(__dirname, './__data__/complex-example.fhir.context.yaml'), 'utf8'), ); -const result = { - body: { - resourceType: 'Bundle', - type: 'transaction', - entry: [ - { - fullUrl: 'urn:uuid:observation-weight', - request: { - url: '/Observation?patient=Patient/pid&category=vital-signs&code=http://loinc.org|29463-7', - method: 'POST', - }, - resource: { - resourceType: 'Observation', - id: undefined, - subject: 'Patient/pid', - status: 'final', - effectiveDateTime: '2024-01-01', - category: [ - { - coding: [ - { - system: 'http://terminology.hl7.org/CodeSystem/observation-category', - code: 'vital-signs', - }, - ], - }, - ], - code: { - coding: [ - { system: 'http://loinc.org', code: '29463-7', display: 'Body Weight' }, - ], - }, - valueQuantity: { - value: 100, - unit: 'kg', - system: 'http://unitsofmeasure.org', - code: 'kg', - }, - }, - }, - { - fullUrl: 'urn:uuid:observation-height', - request: { - url: '/Observation?patient=Patient/pid&category=vital-signs&code=http://loinc.org|8302-2', - method: 'POST', - }, - resource: { - resourceType: 'Observation', - id: undefined, - subject: 'Patient/pid', - status: 'final', - effectiveDateTime: '2024-01-01', - category: [ - { - coding: [ - { - system: 'http://terminology.hl7.org/CodeSystem/observation-category', - code: 'vital-signs', - }, - ], - }, - ], - code: { - coding: [ - { system: 'http://loinc.org', code: '8302-2', display: 'Body Height' }, - ], - }, - valueQuantity: { - value: 190, - unit: 'kg', - system: 'http://unitsofmeasure.org', - code: 'kg', - }, - }, - }, - { - fullUrl: 'urn:uuid:condition-medical-history-0', - request: { url: 'Condition/cond1', method: 'PUT' }, - resource: { - resourceType: 'Condition', - id: 'cond1', - subject: 'Patient/pid', - recordedDate: '2024-01-01', - code: { - coding: [ - { system: 'urn:raw', code: 'hypertension', display: 'Hypertension' }, - ], - text: 'Hypertension', - }, - category: [ - { coding: [{ code: 'medicalHistory', display: 'Medical history' }] }, - ], - }, - }, - { - fullUrl: 'urn:uuid:condition-medical-history-1', - request: { - url: '/Condition?category=medicalHistory&code=urn:raw|fatty-liver&patient=Patient/pid', - method: 'POST', - }, - resource: { - resourceType: 'Condition', - id: undefined, - subject: 'Patient/pid', - recordedDate: '2024-01-01', - code: { - coding: [ - { system: 'urn:raw', code: 'fatty-liver', display: 'Fatty Liver' }, - ], - text: 'Fatty Liver', - }, - category: [ - { coding: [{ code: 'medicalHistory', display: 'Medical history' }] }, - ], - }, - }, - { - fullUrl: 'urn:uuid:condition-medical-history-2', - request: { - url: '/Condition?category=medicalHistory&code=urn:raw|asthma&patient=Patient/pid', - method: 'POST', - }, - resource: { - resourceType: 'Condition', - id: undefined, - subject: 'Patient/pid', - recordedDate: '2024-01-01', - code: { - coding: [{ system: 'urn:raw', code: 'asthma', display: 'Asthma' }], - text: 'Asthma', - }, - category: [ - { coding: [{ code: 'medicalHistory', display: 'Medical history' }] }, - ], - }, - }, - { - request: { url: '/Provenance', method: 'POST' }, - resource: { - resourceType: 'Provenance', - target: [{ uri: 'urn:uuid:observation-weight' }], - recorded: '2024-01-01', - agent: [{ who: 'Organization/orgid' }], - entity: [{ role: 'source', what: 'QuestionnaireResponse/qrid' }], - }, - }, - { - request: { url: '/Provenance', method: 'POST' }, - resource: { - resourceType: 'Provenance', - target: [{ uri: 'urn:uuid:observation-height' }], - recorded: '2024-01-01', - agent: [{ who: 'Organization/orgid' }], - entity: [{ role: 'source', what: 'QuestionnaireResponse/qrid' }], - }, - }, - { - request: { url: '/Provenance', method: 'POST' }, - resource: { - resourceType: 'Provenance', - target: [{ uri: 'urn:uuid:condition-medical-history-0' }], - recorded: '2024-01-01', - agent: [{ who: 'Organization/orgid' }], - entity: [{ role: 'source', what: 'QuestionnaireResponse/qrid' }], - }, - }, - { - request: { url: '/Provenance', method: 'POST' }, - resource: { - resourceType: 'Provenance', - target: [{ uri: 'urn:uuid:condition-medical-history-1' }], - recorded: '2024-01-01', - agent: [{ who: 'Organization/orgid' }], - entity: [{ role: 'source', what: 'QuestionnaireResponse/qrid' }], - }, - }, - { - request: { url: '/Provenance', method: 'POST' }, - resource: { - resourceType: 'Provenance', - target: [{ uri: 'urn:uuid:condition-medical-history-2' }], - recorded: '2024-01-01', - agent: [{ who: 'Organization/orgid' }], - entity: [{ role: 'source', what: 'QuestionnaireResponse/qrid' }], - }, - }, - { request: { url: '/Provenance/prov-hypertension', method: 'DELETE' } }, - { request: { url: '/Condition/cond-hypertension', method: 'DELETE' } }, - ], - }, -}; - -const qr = { - resourceType: 'QuestionnaireResponse', - id: 'qrid', - authored: '2024-01-01', - item: [ - { - linkid: 'root', - item: [ - { - linkId: 'WEIGHT', - answer: { - valueDecimal: 100, - }, - }, - { - linkId: 'HEIGHT', - answer: { - valueDecimal: 190, - }, - }, - { - linkId: 'MEDCOND1', - answer: [ - { - valueCoding: { - system: 'urn:raw', - code: 'hypertension', - display: 'Hypertension', - }, - }, - { - valueCoding: { - system: 'urn:raw', - code: 'fatty-liver', - display: 'Fatty Liver', - }, - }, - ], - }, - { - linkId: 'MEDCOND2', - answer: [ - { - valueCoding: { - system: 'urn:raw', - code: 'asthma', - display: 'Asthma', - }, - }, - ], - }, - ], - }, - ], -}; -const provenances = [ - { - resourceType: 'Provenance', - id: 'prov-hypertension', - target: [ - { - resourceType: 'Condition', - id: 'cond-hypertension', - }, - ], - recorded: '2024-01-01', - agent: [ - { - who: 'Organization/orgid', - }, - ], - entity: [ - { - role: 'source', - what: 'QuestionnaireResponse/qrid', - }, - ], - }, - { - resourceType: 'Provenance', - id: 'prov-flu', - target: [ - { - resourceType: 'Condition', - id: 'cond1', - }, - ], - recorded: '2024-01-01', - agent: [ - { - who: 'Organization/orgid', - }, - ], - entity: [ - { - role: 'source', - what: 'QuestionnaireResponse/qrid', - }, - ], - }, -]; -const observations = []; -const conditions = [ - { - resourceType: 'Condition', - id: 'cond-flu', - subject: 'Patient/pid', - recordedDate: '2024-01-01', - code: { - coding: [ - { - system: 'urn:raw', - code: 'Flu', - display: 'Flu', - }, - ], - text: 'Flu', - }, - category: [ - { - coding: [ - { - code: 'medicalHistory', - display: 'Medical history', - }, - ], - }, - ], - }, - { - resourceType: 'Condition', - id: 'cond1', - subject: 'Patient/pid', - recordedDate: '2024-01-01', - code: { - coding: [ - { - system: 'urn:raw', - code: 'hypertension', - display: 'Hypertension', - }, - ], - text: 'Hypertension', - }, - category: [ - { - coding: [ - { - code: 'medicalHistory', - display: 'Medical history', - }, - ], - }, - ], - }, -]; -const organization = { resourceType: 'Organization', id: 'orgid' }; -const patient = { - resourceType: 'Patient', - id: 'pid', -}; - -const userInvocationTable: UserInvocationTable = { - answers: { - fn: (inputs, linkId: string) => { - return fhirpath.evaluate( - inputs, - `repeat(item).where(linkId='${linkId}').answer.value`, - {}, - fhirpath_r4_model, - ); - }, - arity: { 0: [], 1: ['String'] }, - }, - // Get rid of toString once it's fixed https://github.com/HL7/fhirpath.js/issues/156 - toString: { - fn: (inputs) => fhirpath.evaluate({ x: inputs }, 'x.toString()'), - arity: { 0: [] }, - }, -}; -test('Test real example', () => { +test('Test real example (fhir)', () => { + // Re-wrap to get rid of undefined (reconsider it after #17) expect( - resolveTemplate( - qr as any, + JSON.parse(JSON.stringify(resolveTemplate( + (context as any).QuestionnaireResponse, template, - { - QuestionnaireResponse: qr, - Provenance: provenances, - Condition: conditions, - Observation: observations, - Organization: organization, - Patient: patient, - }, + context, fhirpath_r4_model, - { userInvocationTable }, - ), + ))), ).toStrictEqual(result); }); From bc3ea09d76ceec7ff407c1c23d8780e3467d3d5b Mon Sep 17 00:00:00 2001 From: Vadim Laletin Date: Fri, 28 Feb 2025 17:27:20 +0100 Subject: [PATCH 15/23] Add complex examples tests for python --- python/poetry.lock | 65 ++++++++++++++++++- python/pyproject.toml | 1 + python/tests/conftest.py | 27 ++++++++ python/tests/core/fixtures | 1 + .../tests/core/test_complex_example_aidbox.py | 11 ++++ .../tests/core/test_complex_example_fhir.py | 15 +++++ python/tests/core/test_extract.py | 2 +- .../complex-example.fhir.template.yaml | 2 +- 8 files changed, 121 insertions(+), 3 deletions(-) create mode 120000 python/tests/core/fixtures create mode 100644 python/tests/core/test_complex_example_aidbox.py create mode 100644 python/tests/core/test_complex_example_fhir.py diff --git a/python/poetry.lock b/python/poetry.lock index 8589cc4..fb7ced3 100644 --- a/python/poetry.lock +++ b/python/poetry.lock @@ -649,6 +649,69 @@ files = [ [package.dependencies] six = ">=1.5" +[[package]] +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + [[package]] name = "rich" version = "13.9.4" @@ -818,4 +881,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.9,<4.0" -content-hash = "78afdf4908fd347861ffe496796fc5abac84e70dd3130107f8d074b91a7c146c" +content-hash = "5b351454debbc8f708d483c2c94acfdae788c43f077edf146e662ff852607c1d" diff --git a/python/pyproject.toml b/python/pyproject.toml index 5d9e755..3ebaff5 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -46,6 +46,7 @@ mypy = "^1.15.0" autohooks-plugin-mypy = "^23.10.0" autohooks-plugin-ruff = "^25.2.0" autohooks = "^25.2.0" +pyyaml = "^6.0.2" [tool.poetry.group.test.dependencies] pytest = "^8.3.4" diff --git a/python/tests/conftest.py b/python/tests/conftest.py index e69de29..05b2628 100644 --- a/python/tests/conftest.py +++ b/python/tests/conftest.py @@ -0,0 +1,27 @@ +import os + +import pytest +import yaml + + +@pytest.fixture +def read_fixture(request): + def wrapper(filename): + py_filename = request.module.__file__ + test_dir = os.path.dirname(py_filename) + filepath = os.path.join(test_dir, 'fixtures', filename) + if os.path.isdir(test_dir): + with open(filepath) as file: + return file.read() + raise ValueError(f'File not found {filepath}') + + return wrapper + + +@pytest.fixture +def load_yaml_fixture(read_fixture): + def wrapper(filename): + return yaml.load(read_fixture(filename), Loader=yaml.Loader) + + return wrapper + \ No newline at end of file diff --git a/python/tests/core/fixtures b/python/tests/core/fixtures new file mode 120000 index 0000000..6f7f238 --- /dev/null +++ b/python/tests/core/fixtures @@ -0,0 +1 @@ +../../../tests/__data__ \ No newline at end of file diff --git a/python/tests/core/test_complex_example_aidbox.py b/python/tests/core/test_complex_example_aidbox.py new file mode 100644 index 0000000..7d32293 --- /dev/null +++ b/python/tests/core/test_complex_example_aidbox.py @@ -0,0 +1,11 @@ +from fpml.core.extract import resolve_template + + +def test_complex_example_aidbox(load_yaml_fixture): + context = load_yaml_fixture("complex-example.aidbox.context.yaml") + template = load_yaml_fixture("complex-example.aidbox.template.yaml") + expected_result = load_yaml_fixture("complex-example.aidbox.result.yaml") + + actual_result = resolve_template(context["QuestionnaireResponse"], template, context) + + assert actual_result == expected_result diff --git a/python/tests/core/test_complex_example_fhir.py b/python/tests/core/test_complex_example_fhir.py new file mode 100644 index 0000000..92de4e1 --- /dev/null +++ b/python/tests/core/test_complex_example_fhir.py @@ -0,0 +1,15 @@ +from fhirpathpy.models import models + +from fpml.core.extract import resolve_template + + +def test_complex_example_fhir(load_yaml_fixture): + context = load_yaml_fixture("complex-example.fhir.context.yaml") + template = load_yaml_fixture("complex-example.fhir.template.yaml") + expected_result = load_yaml_fixture("complex-example.fhir.result.yaml") + + actual_result = resolve_template( + context["QuestionnaireResponse"], template, context, fp_options={"model": models["r4"]} + ) + + assert actual_result == expected_result diff --git a/python/tests/core/test_extract.py b/python/tests/core/test_extract.py index 7094d22..3681946 100644 --- a/python/tests/core/test_extract.py +++ b/python/tests/core/test_extract.py @@ -9,7 +9,7 @@ def test_transformation_fails_on_access_props_of_resource_in_strict_mode() -> None: resource: Resource = {"list": [{"key": 1}, {"key": 2}, {"key": 3}]} with pytest.raises(FPMLValidationError): - resolve_template(resource, {"key": "{{ list }}"}, {}, None, None, True) + resolve_template(resource, {"key": "{{ list }}"}, {}, None, True) def test_transformation_for_empty_object_return_empty_object() -> None: diff --git a/tests/__data__/complex-example.fhir.template.yaml b/tests/__data__/complex-example.fhir.template.yaml index 562c79c..eb1811c 100644 --- a/tests/__data__/complex-example.fhir.template.yaml +++ b/tests/__data__/complex-example.fhir.template.yaml @@ -9,7 +9,7 @@ body: - medConditions: "{% for answer in repeat(item).where(linkId in 'MEDCOND1' | 'MEDCOND2').answer.value %}": "{{ %answer }}" - observationEntries: - - "{% if %weight and %height %}": + - "{% if %weight.exists() and %height.exists() %}": "{% assign %}": - observationId: >- {{ From 55b023b387f947bf3b4d30d8981c1d54165b1f4c Mon Sep 17 00:00:00 2001 From: Vadim Laletin Date: Fri, 28 Feb 2025 17:39:08 +0100 Subject: [PATCH 16/23] Setup coverage --- .gitignore | 2 + python/poetry.lock | 104 +++++++++++++++++++++++++++++++++++++++-- python/pyproject.toml | 9 +++- ts/server/package.json | 1 + 4 files changed, 112 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 8914277..9c1254b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ coverage htmlcov .vscode .history +.coverage + diff --git a/python/poetry.lock b/python/poetry.lock index fb7ced3..269adc3 100644 --- a/python/poetry.lock +++ b/python/poetry.lock @@ -125,6 +125,85 @@ files = [ [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} +[[package]] +name = "coverage" +version = "7.6.12" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.9" +groups = ["test"] +files = [ + {file = "coverage-7.6.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:704c8c8c6ce6569286ae9622e534b4f5b9759b6f2cd643f1c1a61f666d534fe8"}, + {file = "coverage-7.6.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ad7525bf0241e5502168ae9c643a2f6c219fa0a283001cee4cf23a9b7da75879"}, + {file = "coverage-7.6.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06097c7abfa611c91edb9e6920264e5be1d6ceb374efb4986f38b09eed4cb2fe"}, + {file = "coverage-7.6.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:220fa6c0ad7d9caef57f2c8771918324563ef0d8272c94974717c3909664e674"}, + {file = "coverage-7.6.12-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3688b99604a24492bcfe1c106278c45586eb819bf66a654d8a9a1433022fb2eb"}, + {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d1a987778b9c71da2fc8948e6f2656da6ef68f59298b7e9786849634c35d2c3c"}, + {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cec6b9ce3bd2b7853d4a4563801292bfee40b030c05a3d29555fd2a8ee9bd68c"}, + {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ace9048de91293e467b44bce0f0381345078389814ff6e18dbac8fdbf896360e"}, + {file = "coverage-7.6.12-cp310-cp310-win32.whl", hash = "sha256:ea31689f05043d520113e0552f039603c4dd71fa4c287b64cb3606140c66f425"}, + {file = "coverage-7.6.12-cp310-cp310-win_amd64.whl", hash = "sha256:676f92141e3c5492d2a1596d52287d0d963df21bf5e55c8b03075a60e1ddf8aa"}, + {file = "coverage-7.6.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e18aafdfb3e9ec0d261c942d35bd7c28d031c5855dadb491d2723ba54f4c3015"}, + {file = "coverage-7.6.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66fe626fd7aa5982cdebad23e49e78ef7dbb3e3c2a5960a2b53632f1f703ea45"}, + {file = "coverage-7.6.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ef01d70198431719af0b1f5dcbefc557d44a190e749004042927b2a3fed0702"}, + {file = "coverage-7.6.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e92ae5a289a4bc4c0aae710c0948d3c7892e20fd3588224ebe242039573bf0"}, + {file = "coverage-7.6.12-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e695df2c58ce526eeab11a2e915448d3eb76f75dffe338ea613c1201b33bab2f"}, + {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d74c08e9aaef995f8c4ef6d202dbd219c318450fe2a76da624f2ebb9c8ec5d9f"}, + {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e995b3b76ccedc27fe4f477b349b7d64597e53a43fc2961db9d3fbace085d69d"}, + {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b1f097878d74fe51e1ddd1be62d8e3682748875b461232cf4b52ddc6e6db0bba"}, + {file = "coverage-7.6.12-cp311-cp311-win32.whl", hash = "sha256:1f7ffa05da41754e20512202c866d0ebfc440bba3b0ed15133070e20bf5aeb5f"}, + {file = "coverage-7.6.12-cp311-cp311-win_amd64.whl", hash = "sha256:e216c5c45f89ef8971373fd1c5d8d1164b81f7f5f06bbf23c37e7908d19e8558"}, + {file = "coverage-7.6.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b172f8e030e8ef247b3104902cc671e20df80163b60a203653150d2fc204d1ad"}, + {file = "coverage-7.6.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:641dfe0ab73deb7069fb972d4d9725bf11c239c309ce694dd50b1473c0f641c3"}, + {file = "coverage-7.6.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e549f54ac5f301e8e04c569dfdb907f7be71b06b88b5063ce9d6953d2d58574"}, + {file = "coverage-7.6.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:959244a17184515f8c52dcb65fb662808767c0bd233c1d8a166e7cf74c9ea985"}, + {file = "coverage-7.6.12-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bda1c5f347550c359f841d6614fb8ca42ae5cb0b74d39f8a1e204815ebe25750"}, + {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ceeb90c3eda1f2d8c4c578c14167dbd8c674ecd7d38e45647543f19839dd6ea"}, + {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f16f44025c06792e0fb09571ae454bcc7a3ec75eeb3c36b025eccf501b1a4c3"}, + {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b076e625396e787448d27a411aefff867db2bffac8ed04e8f7056b07024eed5a"}, + {file = "coverage-7.6.12-cp312-cp312-win32.whl", hash = "sha256:00b2086892cf06c7c2d74983c9595dc511acca00665480b3ddff749ec4fb2a95"}, + {file = "coverage-7.6.12-cp312-cp312-win_amd64.whl", hash = "sha256:7ae6eabf519bc7871ce117fb18bf14e0e343eeb96c377667e3e5dd12095e0288"}, + {file = "coverage-7.6.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:488c27b3db0ebee97a830e6b5a3ea930c4a6e2c07f27a5e67e1b3532e76b9ef1"}, + {file = "coverage-7.6.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d1095bbee1851269f79fd8e0c9b5544e4c00c0c24965e66d8cba2eb5bb535fd"}, + {file = "coverage-7.6.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0533adc29adf6a69c1baa88c3d7dbcaadcffa21afbed3ca7a225a440e4744bf9"}, + {file = "coverage-7.6.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53c56358d470fa507a2b6e67a68fd002364d23c83741dbc4c2e0680d80ca227e"}, + {file = "coverage-7.6.12-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64cbb1a3027c79ca6310bf101014614f6e6e18c226474606cf725238cf5bc2d4"}, + {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79cac3390bfa9836bb795be377395f28410811c9066bc4eefd8015258a7578c6"}, + {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b148068e881faa26d878ff63e79650e208e95cf1c22bd3f77c3ca7b1d9821a3"}, + {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8bec2ac5da793c2685ce5319ca9bcf4eee683b8a1679051f8e6ec04c4f2fd7dc"}, + {file = "coverage-7.6.12-cp313-cp313-win32.whl", hash = "sha256:200e10beb6ddd7c3ded322a4186313d5ca9e63e33d8fab4faa67ef46d3460af3"}, + {file = "coverage-7.6.12-cp313-cp313-win_amd64.whl", hash = "sha256:2b996819ced9f7dbb812c701485d58f261bef08f9b85304d41219b1496b591ef"}, + {file = "coverage-7.6.12-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:299cf973a7abff87a30609879c10df0b3bfc33d021e1adabc29138a48888841e"}, + {file = "coverage-7.6.12-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b467a8c56974bf06e543e69ad803c6865249d7a5ccf6980457ed2bc50312703"}, + {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2458f275944db8129f95d91aee32c828a408481ecde3b30af31d552c2ce284a0"}, + {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a9d8be07fb0832636a0f72b80d2a652fe665e80e720301fb22b191c3434d924"}, + {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d47376a4f445e9743f6c83291e60adb1b127607a3618e3185bbc8091f0467b"}, + {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b95574d06aa9d2bd6e5cc35a5bbe35696342c96760b69dc4287dbd5abd4ad51d"}, + {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ecea0c38c9079570163d663c0433a9af4094a60aafdca491c6a3d248c7432827"}, + {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2251fabcfee0a55a8578a9d29cecfee5f2de02f11530e7d5c5a05859aa85aee9"}, + {file = "coverage-7.6.12-cp313-cp313t-win32.whl", hash = "sha256:eb5507795caabd9b2ae3f1adc95f67b1104971c22c624bb354232d65c4fc90b3"}, + {file = "coverage-7.6.12-cp313-cp313t-win_amd64.whl", hash = "sha256:f60a297c3987c6c02ffb29effc70eadcbb412fe76947d394a1091a3615948e2f"}, + {file = "coverage-7.6.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e7575ab65ca8399c8c4f9a7d61bbd2d204c8b8e447aab9d355682205c9dd948d"}, + {file = "coverage-7.6.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8161d9fbc7e9fe2326de89cd0abb9f3599bccc1287db0aba285cb68d204ce929"}, + {file = "coverage-7.6.12-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a1e465f398c713f1b212400b4e79a09829cd42aebd360362cd89c5bdc44eb87"}, + {file = "coverage-7.6.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f25d8b92a4e31ff1bd873654ec367ae811b3a943583e05432ea29264782dc32c"}, + {file = "coverage-7.6.12-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a936309a65cc5ca80fa9f20a442ff9e2d06927ec9a4f54bcba9c14c066323f2"}, + {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aa6f302a3a0b5f240ee201297fff0bbfe2fa0d415a94aeb257d8b461032389bd"}, + {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f973643ef532d4f9be71dd88cf7588936685fdb576d93a79fe9f65bc337d9d73"}, + {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:78f5243bb6b1060aed6213d5107744c19f9571ec76d54c99cc15938eb69e0e86"}, + {file = "coverage-7.6.12-cp39-cp39-win32.whl", hash = "sha256:69e62c5034291c845fc4df7f8155e8544178b6c774f97a99e2734b05eb5bed31"}, + {file = "coverage-7.6.12-cp39-cp39-win_amd64.whl", hash = "sha256:b01a840ecc25dce235ae4c1b6a0daefb2a203dba0e6e980637ee9c2f6ee0df57"}, + {file = "coverage-7.6.12-pp39.pp310-none-any.whl", hash = "sha256:7e39e845c4d764208e7b8f6a21c541ade741e2c41afabdfa1caa28687a3c98cf"}, + {file = "coverage-7.6.12-py3-none-any.whl", hash = "sha256:eb8668cfbc279a536c633137deeb9435d2962caec279c3f8cf8b91fff6ff8953"}, + {file = "coverage-7.6.12.tar.gz", hash = "sha256:48cfc4641d95d34766ad41d9573cc0f22a48aa88d22657a1fe01dca0dbae4de2"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] + [[package]] name = "exceptiongroup" version = "1.2.2" @@ -634,6 +713,25 @@ tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pytest-cov" +version = "6.0.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.9" +groups = ["test"] +files = [ + {file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"}, + {file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"}, +] + +[package.dependencies] +coverage = {version = ">=7.5", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -655,7 +753,7 @@ version = "6.0.2" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" -groups = ["dev"] +groups = ["test"] files = [ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, @@ -818,7 +916,6 @@ description = "A lil' TOML parser" optional = false python-versions = ">=3.8" groups = ["dev", "test"] -markers = "python_version < \"3.11\"" files = [ {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"}, @@ -853,6 +950,7 @@ files = [ {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] +markers = {dev = "python_version < \"3.11\"", test = "python_full_version <= \"3.11.0a6\""} [[package]] name = "tomlkit" @@ -881,4 +979,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.9,<4.0" -content-hash = "5b351454debbc8f708d483c2c94acfdae788c43f077edf146e662ff852607c1d" +content-hash = "49e3c0c581fddad815683a0ad583a5936c6b0bd87b59359441149ace2e68c01d" diff --git a/python/pyproject.toml b/python/pyproject.toml index 3ebaff5..f3a4253 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -46,10 +46,11 @@ mypy = "^1.15.0" autohooks-plugin-mypy = "^23.10.0" autohooks-plugin-ruff = "^25.2.0" autohooks = "^25.2.0" -pyyaml = "^6.0.2" [tool.poetry.group.test.dependencies] pytest = "^8.3.4" +pytest-cov = "^6.0.0" +pyyaml = "^6.0.2" [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"] @@ -70,3 +71,9 @@ check_untyped_defs = true [tool.autohooks] mode = "poetry" pre-commit = ["autohooks.plugins.mypy", "autohooks.plugins.ruff.format", "autohooks.plugins.ruff.check"] + + +[tool.pytest.ini_options] +addopts = "-ra --color=yes --cov fpml --cov-report html --doctest-modules" +log_cli = true +log_cli_level = "WARNING" \ No newline at end of file diff --git a/ts/server/package.json b/ts/server/package.json index c017c60..1839ba1 100644 --- a/ts/server/package.json +++ b/ts/server/package.json @@ -66,6 +66,7 @@ "**/*.(t|j)s" ], "coverageDirectory": "../coverage", + "coverageReporters": ["text", "html"], "testEnvironment": "node" } } From 143f583e30a898af6bcfb17d7a763f7fc0621258 Mon Sep 17 00:00:00 2001 From: Vadim Laletin Date: Fri, 28 Feb 2025 21:21:48 +0100 Subject: [PATCH 17/23] Update readme --- README.md | 54 +++++++++++++++++++++++++++++++++++++++++++----- python/README.md | 29 ++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 257c515..9a5e863 100644 --- a/README.md +++ b/README.md @@ -547,16 +547,18 @@ that will be transformed into: ## Examples -See real-life examples of mappers for [FHIR](https://github.com/beda-software/FHIRPathMappingLanguage/blob/main/ts/server/src/utils/__data__/complex-example.fhir.yaml) and [Aidbox](https://github.com/beda-software/FHIRPathMappingLanguage/blob/main/ts/server/src/utils/__data__/complex-example.aidbox.yaml) +See real-life examples of mappers for [FHIR](https://github.com/beda-software/FHIRPathMappingLanguage/blob/main/tests/__data__/complex-example.fhir.yaml) and [Aidbox](https://github.com/beda-software/FHIRPathMappingLanguage/blob/main/tests/__data__/complex-example.aidbox.yaml) and other usage in [unit tests](https://github.com/beda-software/FHIRPathMappingLanguage/tree/main/ts/server/src/utils). ## Reference implementation -TypeScript implementation that supports all the specification is already available [in this repository](https://github.com/beda-software/FHIRPathMappingLanguage/tree/main/server). +### TypeScript + +TypeScript implementation that supports all the specification is already available [in this repository](https://github.com/beda-software/FHIRPathMappingLanguage/tree/main/ts/server). Also, it is packed into a [docker image](https://hub.docker.com/r/bedasoftware/fhirpath-extract) to use as a microservice. -### Usage +#### Usage POST /r4/parse-template @@ -577,7 +579,7 @@ POST /r4/parse-template } ``` -### Strict mode +#### Strict mode FHIRPath provides a way of accessing the `resource` variables without the percent sign. It potentially leads to the issues made by typos in the variable names. @@ -603,4 +605,46 @@ POST /r4/parse-template?strict=true "status": "completed" } } -``` \ No newline at end of file +``` + +### Python + +Python implementation that supports all the specification is already available [in this repository](https://github.com/beda-software/FHIRPathMappingLanguage/tree/main/python). +Also, it's available as a PyPI package under the name `fpml` and can be installed using +``` +pip install fpml +``` + +#### Usage + +```python +from fpml import resolve_template + + +resource = { + "resourceType": "QuestionnaireResponse", + "status": "completed", + "item": [ + { + "linkId": "name", + "answer": [ + { + "valueString": "Name" + } + ] + } + ] +} + +template = { + "resourceType": "Patient", + "name": "{{ item.where(linkId='name').answer.valueString }}" +} + +context = {} + +result = resolve_template(resource, template, context) + +print(result) +# {'resourceType': 'Patient', 'name': 'Name'} +``` diff --git a/python/README.md b/python/README.md index fcf6749..61e8c17 100644 --- a/python/README.md +++ b/python/README.md @@ -10,8 +10,37 @@ pip install fpml ```python from fpml import resolve_template + + +resource = { + "resourceType": "QuestionnaireResponse", + "status": "completed", + "item": [ + { + "linkId": "name", + "answer": [ + { + "valueString": "Name" + } + ] + } + ] +} + +template = { + "resourceType": "Patient", + "name": "{{ item.where(linkId='name').answer.valueString }}" +} + +context = {} + +result = resolve_template(resource, template, context) + +print(result) +# {'resourceType': 'Patient', 'name': 'Name'} ``` + ## Development In `./python` directory: From 0f9f053877745798c68ae5225550931a90425793 Mon Sep 17 00:00:00 2001 From: Vadim Laletin Date: Fri, 28 Feb 2025 21:41:54 +0100 Subject: [PATCH 18/23] Configure for pypi --- .gitignore | 2 +- python/fpml/__init__.py | 8 ++++++++ python/pyproject.toml | 6 +++--- ts/server/package.json | 8 ++++---- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 9c1254b..c620c5a 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,4 @@ htmlcov .vscode .history .coverage - +python/dist diff --git a/python/fpml/__init__.py b/python/fpml/__init__.py index c429df3..b514c47 100644 --- a/python/fpml/__init__.py +++ b/python/fpml/__init__.py @@ -1,4 +1,12 @@ +import importlib.metadata + from .core.exceptions import FPMLValidationError from .core.extract import resolve_template +__title__ = "fpml" +__version__ = importlib.metadata.version("fpml") +__author__ = "beda.software" +__license__ = "MIT" +__copyright__ = "Copyright 2025 beda.software" + __all__ = ["FPMLValidationError", "resolve_template"] diff --git a/python/pyproject.toml b/python/pyproject.toml index f3a4253..d88e57b 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -13,13 +13,13 @@ dynamic = ["version", "classifiers"] dependencies = ["fhirpathpy (>=1.2.1,<2.0.0)"] [project.urls] -homepage = "https://github.com/beda-software/FHIRPathMappingLanguage/tree/main/python/" +homepage = "https://github.com/beda-software/FHIRPathMappingLanguage/tree/main/python" repository = "https://github.com/beda-software/FHIRPathMappingLanguage/tree/main/python" documentation = "https://github.com/beda-software/FHIRPathMappingLanguage/tree/main/python/README.md" "Bug Tracker" = "https://github.com/beda-software/FHIRPathMappingLanguage/issues" [tool.poetry] -version = "0.1.0" +version = "0.0.1" packages = [{ include = "fpml" }] classifiers = [ "Development Status :: 5 - Production/Stable", @@ -35,7 +35,7 @@ classifiers = [ "Programming Language :: Python :: 3.13", "Topic :: Software Development :: Libraries :: Python Modules", ] -readme = ["README.md", "../README.md"] +readme = ["README.md"] [tool.poetry.dependencies] python = ">=3.9,<4.0" diff --git a/ts/server/package.json b/ts/server/package.json index 1839ba1..fb7b3c9 100644 --- a/ts/server/package.json +++ b/ts/server/package.json @@ -1,10 +1,10 @@ { - "name": "server", + "name": "fpml-server", "version": "0.0.1", - "description": "", - "author": "", + "description": "The FHIRPath mapping language is a data DSL designed to convert data from QuestionnaireResponse (and not only) to any FHIR Resource.", + "author": "beda.software", "private": true, - "license": "UNLICENSED", + "license": "MIT", "scripts": { "build": "nest build", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", From 3e00536e8687fe62d32e0b892dfa0df18691ea8f Mon Sep 17 00:00:00 2001 From: Vadim Laletin Date: Fri, 28 Feb 2025 21:54:03 +0100 Subject: [PATCH 19/23] Add test step for python implementation --- .github/workflows/github-actions.yml | 44 ++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index b023c5a..e83df7b 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -3,7 +3,7 @@ on: [push, pull_request] env: BUILD_IMAGE: bedasoftware/fhirpath-extract:main jobs: - Build: + build-and-test-fpml-server-ts: runs-on: ubuntu-latest steps: - name: Copy repository @@ -29,8 +29,8 @@ jobs: run: yarn test:e2e working-directory: ts/server - Publish: - needs: Build + publish-fpml-server-ts-to-dockerhub: + needs: build-and-test-fpml-server-ts if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: @@ -48,3 +48,41 @@ jobs: docker buildx build --platform linux/arm64,linux/amd64 --push --tag ${{ env.BUILD_IMAGE }} . working-directory: ts/server + + build-and-test-fpml-py: + runs-on: ubuntu-latest + strategy: + matrix: + include: + - python-version: 3.9 + - python-version: "3.10" + - python-version: "3.11" + - python-version: "3.12" + - python-version: "3.13" + env: + PYTHON: ${{ matrix.python-version }} + steps: + - uses: actions/checkout@v3 + - name: Run tests + run: pytest --coverage + shell: bash + + publish-fpml-py-to-pypi: + needs: build-and-test-fpml-py + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + permissions: + packages: write + contents: read + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.11 + uses: actions/setup-python@v3 + with: + python-version: "3.11" + - name: Install wheel and build + run: pip install wheel build + - name: Build a binary wheel and a source tarball + run: poetry build + - name: Publish package + uses: pypa/gh-action-pypi-publish@release/v1 From cb30a48174f7ddd3dce5252d302ac1d29653ee2a Mon Sep 17 00:00:00 2001 From: Vadim Laletin Date: Fri, 28 Feb 2025 21:58:01 +0100 Subject: [PATCH 20/23] Update workflow --- .github/workflows/github-actions.yml | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index e83df7b..5c83cf5 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -63,9 +63,21 @@ jobs: PYTHON: ${{ matrix.python-version }} steps: - uses: actions/checkout@v3 + - name: Python ${{ matrix.python-version }} sample + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install poetry + poetry --project=./python install + - name: Run lint + run: poetry --project=./python run ruff check + - name: Run typecheck + run: poetry --project=./python run mypy - name: Run tests - run: pytest --coverage - shell: bash + run: poetry --project=./python run pytest --coverage publish-fpml-py-to-pypi: needs: build-and-test-fpml-py From a573d7e536de2e54add0f718c88b8bb20923096e Mon Sep 17 00:00:00 2001 From: Vadim Laletin Date: Fri, 28 Feb 2025 22:03:57 +0100 Subject: [PATCH 21/23] Configure proper commands for lint/typecheck --- .github/workflows/github-actions.yml | 4 ++-- examples/python/pydian_repeatable.py | 2 +- python/fpml/core/types.py | 1 + python/poetry.lock | 14 +++++++++++++- python/pyproject.toml | 2 +- python/tests/core/test_complex_example_fhir.py | 2 +- 6 files changed, 19 insertions(+), 6 deletions(-) diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index 5c83cf5..252c679 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -73,9 +73,9 @@ jobs: pip install poetry poetry --project=./python install - name: Run lint - run: poetry --project=./python run ruff check + run: poetry --project=./python run ruff check ./python - name: Run typecheck - run: poetry --project=./python run mypy + run: poetry --project=./python run mypy ./python - name: Run tests run: poetry --project=./python run pytest --coverage diff --git a/examples/python/pydian_repeatable.py b/examples/python/pydian_repeatable.py index b796749..6a96b3a 100644 --- a/examples/python/pydian_repeatable.py +++ b/examples/python/pydian_repeatable.py @@ -1,4 +1,4 @@ -from pydian import DROP, Mapper, get +from pydian import get def map(qr): diff --git a/python/fpml/core/types.py b/python/fpml/core/types.py index 4732b6f..f4949ab 100644 --- a/python/fpml/core/types.py +++ b/python/fpml/core/types.py @@ -1,3 +1,4 @@ +# noqa: A005 from typing import Any, Callable, Optional, TypedDict, Union Resource = dict[str, Any] diff --git a/python/poetry.lock b/python/poetry.lock index 269adc3..b3af97c 100644 --- a/python/poetry.lock +++ b/python/poetry.lock @@ -964,6 +964,18 @@ files = [ {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, ] +[[package]] +name = "types-pyyaml" +version = "6.0.12.20241230" +description = "Typing stubs for PyYAML" +optional = false +python-versions = ">=3.8" +groups = ["test"] +files = [ + {file = "types_PyYAML-6.0.12.20241230-py3-none-any.whl", hash = "sha256:fa4d32565219b68e6dee5f67534c722e53c00d1cfc09c435ef04d7353e1e96e6"}, + {file = "types_pyyaml-6.0.12.20241230.tar.gz", hash = "sha256:7f07622dbd34bb9c8b264fe860a17e0efcad00d50b5f27e93984909d9363498c"}, +] + [[package]] name = "typing-extensions" version = "4.12.2" @@ -979,4 +991,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.9,<4.0" -content-hash = "49e3c0c581fddad815683a0ad583a5936c6b0bd87b59359441149ace2e68c01d" +content-hash = "58d5217a9b7b09e59c595215e3a79375f2b449eedbc92aafc510f61c6f4e0d1a" diff --git a/python/pyproject.toml b/python/pyproject.toml index d88e57b..5153cbb 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -51,6 +51,7 @@ autohooks = "^25.2.0" pytest = "^8.3.4" pytest-cov = "^6.0.0" pyyaml = "^6.0.2" +types-PyYAML = "^6.0.12" [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"] @@ -72,7 +73,6 @@ check_untyped_defs = true mode = "poetry" pre-commit = ["autohooks.plugins.mypy", "autohooks.plugins.ruff.format", "autohooks.plugins.ruff.check"] - [tool.pytest.ini_options] addopts = "-ra --color=yes --cov fpml --cov-report html --doctest-modules" log_cli = true diff --git a/python/tests/core/test_complex_example_fhir.py b/python/tests/core/test_complex_example_fhir.py index 92de4e1..7c546d9 100644 --- a/python/tests/core/test_complex_example_fhir.py +++ b/python/tests/core/test_complex_example_fhir.py @@ -1,4 +1,4 @@ -from fhirpathpy.models import models +from fhirpathpy.models import models # type: ignore from fpml.core.extract import resolve_template From 74a3408b387caafaffd9f101b3034f26aa1e4d31 Mon Sep 17 00:00:00 2001 From: Vadim Laletin Date: Fri, 28 Feb 2025 22:08:06 +0100 Subject: [PATCH 22/23] Fix coverage --- .github/workflows/github-actions.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index 252c679..764d1f5 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -77,7 +77,7 @@ jobs: - name: Run typecheck run: poetry --project=./python run mypy ./python - name: Run tests - run: poetry --project=./python run pytest --coverage + run: poetry --project=./python run pytest --cov publish-fpml-py-to-pypi: needs: build-and-test-fpml-py From a45265a6f060f1632f9684d77f4dce2e06e53dd4 Mon Sep 17 00:00:00 2001 From: Vadim Laletin Date: Fri, 28 Feb 2025 22:26:14 +0100 Subject: [PATCH 23/23] Make reusable test python flow --- .github/workflows/github-actions.yml | 58 ++++------------------------ .github/workflows/publish.yml | 26 +++++++++++++ .github/workflows/test-python.yml | 32 +++++++++++++++ 3 files changed, 66 insertions(+), 50 deletions(-) create mode 100644 .github/workflows/publish.yml create mode 100644 .github/workflows/test-python.yml diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index 764d1f5..242ea19 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -1,5 +1,10 @@ name: github-actions -on: [push, pull_request] +on: + push: + branches: + - "*" + tags-ignore: + - "v*.*.*" env: BUILD_IMAGE: bedasoftware/fhirpath-extract:main jobs: @@ -49,52 +54,5 @@ jobs: --push --tag ${{ env.BUILD_IMAGE }} . working-directory: ts/server - build-and-test-fpml-py: - runs-on: ubuntu-latest - strategy: - matrix: - include: - - python-version: 3.9 - - python-version: "3.10" - - python-version: "3.11" - - python-version: "3.12" - - python-version: "3.13" - env: - PYTHON: ${{ matrix.python-version }} - steps: - - uses: actions/checkout@v3 - - name: Python ${{ matrix.python-version }} sample - uses: actions/setup-python@v3 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install poetry - poetry --project=./python install - - name: Run lint - run: poetry --project=./python run ruff check ./python - - name: Run typecheck - run: poetry --project=./python run mypy ./python - - name: Run tests - run: poetry --project=./python run pytest --cov - - publish-fpml-py-to-pypi: - needs: build-and-test-fpml-py - if: github.ref == 'refs/heads/main' - runs-on: ubuntu-latest - permissions: - packages: write - contents: read - steps: - - uses: actions/checkout@v3 - - name: Set up Python 3.11 - uses: actions/setup-python@v3 - with: - python-version: "3.11" - - name: Install wheel and build - run: pip install wheel build - - name: Build a binary wheel and a source tarball - run: poetry build - - name: Publish package - uses: pypa/gh-action-pypi-publish@release/v1 + test-fpml-py: + uses: ./.github/workflows/test-python.yml \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..91d4b6e --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,26 @@ +name: publish +on: + push: + tags: + - "v*.*.*" +jobs: + test-fpml-py: + uses: ./.github/workflows/test-python.yml + + publish-fpml-py-to-pypi: + needs: test-fpml-py + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.11 + uses: actions/setup-python@v3 + with: + python-version: "3.11" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install poetry + - name: Build a binary wheel and a source tarball + run: poetry --project=./python build + - name: Publish package + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/test-python.yml b/.github/workflows/test-python.yml new file mode 100644 index 0000000..82b92e7 --- /dev/null +++ b/.github/workflows/test-python.yml @@ -0,0 +1,32 @@ +name: test-python +on: [workflow_call] +jobs: + test-fpml-py: + runs-on: ubuntu-latest + strategy: + matrix: + include: + - python-version: 3.9 + - python-version: "3.10" + - python-version: "3.11" + - python-version: "3.12" + - python-version: "3.13" + env: + PYTHON: ${{ matrix.python-version }} + steps: + - uses: actions/checkout@v3 + - name: Python ${{ matrix.python-version }} sample + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install poetry + poetry --project=./python install + - name: Run lint + run: poetry --project=./python run ruff check ./python + - name: Run typecheck + run: poetry --project=./python run mypy ./python + - name: Run tests + run: poetry --project=./python run pytest --cov