From 5b524b0b3d7a9b4ba527322f64d664cf16dfb7da Mon Sep 17 00:00:00 2001 From: moritzsommer Date: Wed, 29 May 2024 05:13:10 +0200 Subject: [PATCH 01/18] Add test cases --- .github/workflows/ci.yml | 29 +++++++ .gitignore | 1 + config.ini.default | 2 +- requirements.txt | 2 + semantic_id_resolver/resolver.py | 9 ++- semantic_id_resolver/service.py | 5 +- test/__init__.py | 0 test/test_resolving_service.py | 117 ++++++++++++++++++++++++++++ test_resources/config.ini | 9 +++ test_resources/debug_endpoints.json | 4 + 10 files changed, 171 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 test/__init__.py create mode 100644 test/test_resolving_service.py create mode 100644 test_resources/config.ini create mode 100644 test_resources/debug_endpoints.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..757a553 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: test + +on: + push: + branches: + - wip/testing + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + architecture: x64 + + - name: Install Python dependencies + run: pip install -r requirements.txt + + - name: Run Python Tests + run: python -m unittest discover \ No newline at end of file diff --git a/.gitignore b/.gitignore index 9c92902..e7af935 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ # Typical virtualenv dir /venv/ +/.venv/ # IDE settings /.idea/ diff --git a/config.ini.default b/config.ini.default index 1d957d6..87f96bf 100644 --- a/config.ini.default +++ b/config.ini.default @@ -6,4 +6,4 @@ port=8125 fallback_semantic_matching_service=https://example.org/semantic_matching_service eclass_semantic_matching_service=https://example.org/semantic_matching_service cdd_semantic_matching_service=https://example.org/semantic_matching_service -debug_semantic_matching_service_endpoints=../debug_endpoints.json \ No newline at end of file +debug_semantic_matching_service_endpoints=../debug_endpoints.json diff --git a/requirements.txt b/requirements.txt index 33bfc16..aa9f52e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,5 @@ fastapi>=0.95.0 pydantic>=1.10 uvicorn>=0.21.1 dnspython>=2.4.2 +requests>=2.32.2 +setuptools>=68.2.0 \ No newline at end of file diff --git a/semantic_id_resolver/resolver.py b/semantic_id_resolver/resolver.py index cac45f5..b0fc090 100644 --- a/semantic_id_resolver/resolver.py +++ b/semantic_id_resolver/resolver.py @@ -39,8 +39,8 @@ def is_iri_not_irdi(semantic_id: str) -> Optional[bool]: # Check if the scheme is present, which indicates it's a URI if parsed_url.scheme: return True - # Check if there is a colon in the netloc, which could indicate an IRI - elif ':' in parsed_url.netloc: + # TODO IRDI parser + elif "#" in parsed_url.fragment: return False # If neither condition is met, return None else: @@ -103,9 +103,10 @@ def find_semantic_matching_service(self, semantic_id: str) -> Optional[str]: return debug_endpoint # Check for IRI and IRDI - if is_iri_not_irdi(semantic_id) is True: + is_iri = is_iri_not_irdi(semantic_id) + if is_iri is True: return _iri_find_semantic_matching_service(semantic_id) - elif is_iri_not_irdi(semantic_id) is False: + elif is_iri is False: return self._irdi_find_semantic_matching_service(semantic_id) else: return None diff --git a/semantic_id_resolver/service.py b/semantic_id_resolver/service.py index 0b57130..16289d4 100644 --- a/semantic_id_resolver/service.py +++ b/semantic_id_resolver/service.py @@ -57,7 +57,7 @@ def get_semantic_matching_service( endpoint = found_endpoint return SMSResponse( semantic_matching_service_endpoint=endpoint, - meta_information={} # Todo + meta_information={} # TODO metainformation ) @@ -82,7 +82,7 @@ def get_semantic_matching_service( DEBUG_ENDPOINTS = resolver.DebugSemanticMatchingServiceEndpoints.from_file( config["RESOLVER"]["debug_semantic_matching_service_endpoints"] ) - print(f"USING DEBUG ENDPOINTS FROM {config["RESOLVER"]["debug_semantic_matching_service_endpoints"]}") + print(f"USING DEBUG ENDPOINTS FROM {config['RESOLVER']['debug_semantic_matching_service_endpoints']}") except FileNotFoundError: DEBUG_ENDPOINTS = resolver.DebugSemanticMatchingServiceEndpoints(debug_endpoints={}) @@ -97,4 +97,5 @@ def get_semantic_matching_service( APP.include_router( SEMANTIC_ID_RESOLVING_SERVICE.router ) + # TODO read host from config uvicorn.run(APP, host="127.0.0.1", port=int(config["SERVICE"]["PORT"])) diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/test_resolving_service.py b/test/test_resolving_service.py new file mode 100644 index 0000000..ae19fd5 --- /dev/null +++ b/test/test_resolving_service.py @@ -0,0 +1,117 @@ +import os +import configparser +from typing import Dict +import multiprocessing +import requests +import unittest + +from fastapi import FastAPI +import uvicorn + +from semantic_id_resolver import resolver +from semantic_id_resolver.service import SemanticIdResolvingService, SMSRequest + + +def run_server(): + # Load test configuration + config = configparser.ConfigParser() + config.read([ + os.path.abspath(os.path.join(os.path.dirname(__file__), "../test_resources/config.ini")), + ]) + + # Define test configuration + IRDI_MATCHER_DICT: Dict[resolver.IRDISources, str] = { + resolver.IRDISources.ECLASS: config["RESOLVER"]["eclass_semantic_matching_service"], + resolver.IRDISources.IEC_CDD: config["RESOLVER"]["cdd_semantic_matching_service"] + } + + try: + DEBUG_ENDPOINTS = resolver.DebugSemanticMatchingServiceEndpoints.from_file( + config["RESOLVER"]["debug_semantic_matching_service_endpoints"] + ) + print(f"USING DEBUG ENDPOINTS FROM {config['RESOLVER']['debug_semantic_matching_service_endpoints']}") + except FileNotFoundError: + DEBUG_ENDPOINTS = resolver.DebugSemanticMatchingServiceEndpoints(debug_endpoints={}) + + # Mock SemanticIdResolvingService for testing + mock_resolver = resolver.SemanticIdResolver(IRDI_MATCHER_DICT, DEBUG_ENDPOINTS) + semantic_id_resolver_service = SemanticIdResolvingService( + endpoint=config["SERVICE"]["endpoint"], + fallback_semantic_matching_service_endpoint=config["RESOLVER"]["fallback_semantic_matching_service"], + semantic_id_resolver=mock_resolver + ) + + app = FastAPI() + app.include_router(semantic_id_resolver_service.router) + uvicorn.run(app, host=str(config["SERVICE"]["ENDPOINT"]), port=int(config["SERVICE"]["PORT"]), log_level="error") + + +class TestSemanticMatchingService(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.server_process = multiprocessing.Process(target=run_server) + cls.server_process.start() + + @classmethod + def tearDownClass(cls): + cls.server_process.terminate() + cls.server_process.join() + + def test_semantic_matching_service_iri(self): + # TODO deposit DNS record + sms_request = SMSRequest( + semantic_id="foo://example.org:1234/over/there?name=bar#page=3", + ) + response = requests.get( + "http://127.0.0.1:8125/get_semantic_matching_service", + data=sms_request.model_dump_json() + ) + self.assertEqual( + "https://example.org/fallback_semantic_matching_service", + response.json()["semantic_matching_service_endpoint"] + ) + + def test_semantic_matching_service_irdi_eclass(self): + sms_request = SMSRequest( + semantic_id="0173-1#01-ACK323#017", + ) + response = requests.get( + "http://127.0.0.1:8125/get_semantic_matching_service", + data=sms_request.model_dump_json() + ) + self.assertEqual( + "https://example.org/eclass_semantic_matching_service", + response.json()["semantic_matching_service_endpoint"] + ) + + def test_semantic_matching_service_irdi_cdd(self): + sms_request = SMSRequest( + semantic_id="0112-1#01-ACK323#017", + ) + response = requests.get( + "http://127.0.0.1:8125/get_semantic_matching_service", + data=sms_request.model_dump_json() + ) + self.assertEqual( + "https://example.org/cdd_semantic_matching_service", + response.json()["semantic_matching_service_endpoint"] + ) + + def test_semantic_matching_service_fallback(self): + sms_request = SMSRequest( + semantic_id="nothing", + ) + response = requests.get( + "http://127.0.0.1:8125/get_semantic_matching_service", + data=sms_request.model_dump_json() + ) + self.assertEqual( + "https://example.org/fallback_semantic_matching_service", + response.json()["semantic_matching_service_endpoint"] + ) + + # TODO check debug endpoints + + +if __name__ == '__main__': + unittest.main() diff --git a/test_resources/config.ini b/test_resources/config.ini new file mode 100644 index 0000000..ee3edbd --- /dev/null +++ b/test_resources/config.ini @@ -0,0 +1,9 @@ +[SERVICE] +endpoint=127.0.0.1 +port=8125 + +[RESOLVER] +fallback_semantic_matching_service=https://example.org/fallback_semantic_matching_service +eclass_semantic_matching_service=https://example.org/eclass_semantic_matching_service +cdd_semantic_matching_service=https://example.org/cdd_semantic_matching_service +debug_semantic_matching_service_endpoints=../debug_endpoints.json diff --git a/test_resources/debug_endpoints.json b/test_resources/debug_endpoints.json new file mode 100644 index 0000000..c5395a9 --- /dev/null +++ b/test_resources/debug_endpoints.json @@ -0,0 +1,4 @@ +{ + "https://example.org/semanticIDone": "http://localhost:1234/semantic_matching_service", + "https://example.org/semanticIDtwo": "http://localhost:1234/semantic_matching_service" +} From 6ac4172d6e5685c4047b3be454a674b269626829 Mon Sep 17 00:00:00 2001 From: moritzsommer Date: Wed, 29 May 2024 05:26:03 +0200 Subject: [PATCH 02/18] Adapt ci --- test/test_resolving_service.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/test_resolving_service.py b/test/test_resolving_service.py index ae19fd5..8343296 100644 --- a/test/test_resolving_service.py +++ b/test/test_resolving_service.py @@ -63,7 +63,7 @@ def test_semantic_matching_service_iri(self): semantic_id="foo://example.org:1234/over/there?name=bar#page=3", ) response = requests.get( - "http://127.0.0.1:8125/get_semantic_matching_service", + "http://localhost:8125/get_semantic_matching_service", data=sms_request.model_dump_json() ) self.assertEqual( @@ -76,7 +76,7 @@ def test_semantic_matching_service_irdi_eclass(self): semantic_id="0173-1#01-ACK323#017", ) response = requests.get( - "http://127.0.0.1:8125/get_semantic_matching_service", + "http://localhost:8125/get_semantic_matching_service", data=sms_request.model_dump_json() ) self.assertEqual( @@ -89,7 +89,7 @@ def test_semantic_matching_service_irdi_cdd(self): semantic_id="0112-1#01-ACK323#017", ) response = requests.get( - "http://127.0.0.1:8125/get_semantic_matching_service", + "http://localhost:8125/get_semantic_matching_service", data=sms_request.model_dump_json() ) self.assertEqual( @@ -102,7 +102,7 @@ def test_semantic_matching_service_fallback(self): semantic_id="nothing", ) response = requests.get( - "http://127.0.0.1:8125/get_semantic_matching_service", + "http://localhost:8125/get_semantic_matching_service", data=sms_request.model_dump_json() ) self.assertEqual( From 40d4c265dddb3343686cdf3be4e035973ac108a8 Mon Sep 17 00:00:00 2001 From: moritzsommer Date: Wed, 29 May 2024 05:37:41 +0200 Subject: [PATCH 03/18] Adapt ci --- test/test_resolving_service.py | 8 ++++---- test_resources/config.ini | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test/test_resolving_service.py b/test/test_resolving_service.py index 8343296..539bf10 100644 --- a/test/test_resolving_service.py +++ b/test/test_resolving_service.py @@ -63,7 +63,7 @@ def test_semantic_matching_service_iri(self): semantic_id="foo://example.org:1234/over/there?name=bar#page=3", ) response = requests.get( - "http://localhost:8125/get_semantic_matching_service", + "http://localhost:8000/get_semantic_matching_service", data=sms_request.model_dump_json() ) self.assertEqual( @@ -76,7 +76,7 @@ def test_semantic_matching_service_irdi_eclass(self): semantic_id="0173-1#01-ACK323#017", ) response = requests.get( - "http://localhost:8125/get_semantic_matching_service", + "http://localhost:8000/get_semantic_matching_service", data=sms_request.model_dump_json() ) self.assertEqual( @@ -89,7 +89,7 @@ def test_semantic_matching_service_irdi_cdd(self): semantic_id="0112-1#01-ACK323#017", ) response = requests.get( - "http://localhost:8125/get_semantic_matching_service", + "http://localhost:8000/get_semantic_matching_service", data=sms_request.model_dump_json() ) self.assertEqual( @@ -102,7 +102,7 @@ def test_semantic_matching_service_fallback(self): semantic_id="nothing", ) response = requests.get( - "http://localhost:8125/get_semantic_matching_service", + "http://localhost:8000/get_semantic_matching_service", data=sms_request.model_dump_json() ) self.assertEqual( diff --git a/test_resources/config.ini b/test_resources/config.ini index ee3edbd..7c2ae7a 100644 --- a/test_resources/config.ini +++ b/test_resources/config.ini @@ -1,6 +1,6 @@ [SERVICE] endpoint=127.0.0.1 -port=8125 +port=8000 [RESOLVER] fallback_semantic_matching_service=https://example.org/fallback_semantic_matching_service From 018874a95833ba8f52ea823752503d8354082f3a Mon Sep 17 00:00:00 2001 From: moritzsommer Date: Wed, 29 May 2024 05:47:09 +0200 Subject: [PATCH 04/18] Adapt ci --- test/test_resolving_service.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/test_resolving_service.py b/test/test_resolving_service.py index 539bf10..7fddb47 100644 --- a/test/test_resolving_service.py +++ b/test/test_resolving_service.py @@ -43,6 +43,8 @@ def run_server(): app = FastAPI() app.include_router(semantic_id_resolver_service.router) + print(str(config["SERVICE"]["ENDPOINT"])) + print(int(config["SERVICE"]["PORT"])) uvicorn.run(app, host=str(config["SERVICE"]["ENDPOINT"]), port=int(config["SERVICE"]["PORT"]), log_level="error") From ef91c27002aa1283d765c900e24cfd6b54d856c0 Mon Sep 17 00:00:00 2001 From: moritzsommer Date: Wed, 29 May 2024 05:59:28 +0200 Subject: [PATCH 05/18] Adapt ci --- test/test_resolving_service.py | 35 +++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/test/test_resolving_service.py b/test/test_resolving_service.py index 7fddb47..0e5ad15 100644 --- a/test/test_resolving_service.py +++ b/test/test_resolving_service.py @@ -43,24 +43,16 @@ def run_server(): app = FastAPI() app.include_router(semantic_id_resolver_service.router) - print(str(config["SERVICE"]["ENDPOINT"])) - print(int(config["SERVICE"]["PORT"])) uvicorn.run(app, host=str(config["SERVICE"]["ENDPOINT"]), port=int(config["SERVICE"]["PORT"]), log_level="error") class TestSemanticMatchingService(unittest.TestCase): - @classmethod - def setUpClass(cls): - cls.server_process = multiprocessing.Process(target=run_server) - cls.server_process.start() - - @classmethod - def tearDownClass(cls): - cls.server_process.terminate() - cls.server_process.join() def test_semantic_matching_service_iri(self): # TODO deposit DNS record + server_process = multiprocessing.Process(target=run_server) + server_process.start() + sms_request = SMSRequest( semantic_id="foo://example.org:1234/over/there?name=bar#page=3", ) @@ -73,7 +65,13 @@ def test_semantic_matching_service_iri(self): response.json()["semantic_matching_service_endpoint"] ) + server_process.terminate() + server_process.join() + def test_semantic_matching_service_irdi_eclass(self): + server_process = multiprocessing.Process(target=run_server) + server_process.start() + sms_request = SMSRequest( semantic_id="0173-1#01-ACK323#017", ) @@ -86,7 +84,13 @@ def test_semantic_matching_service_irdi_eclass(self): response.json()["semantic_matching_service_endpoint"] ) + server_process.terminate() + server_process.join() + def test_semantic_matching_service_irdi_cdd(self): + server_process = multiprocessing.Process(target=run_server) + server_process.start() + sms_request = SMSRequest( semantic_id="0112-1#01-ACK323#017", ) @@ -99,7 +103,13 @@ def test_semantic_matching_service_irdi_cdd(self): response.json()["semantic_matching_service_endpoint"] ) + server_process.terminate() + server_process.join() + def test_semantic_matching_service_fallback(self): + server_process = multiprocessing.Process(target=run_server) + server_process.start() + sms_request = SMSRequest( semantic_id="nothing", ) @@ -112,6 +122,9 @@ def test_semantic_matching_service_fallback(self): response.json()["semantic_matching_service_endpoint"] ) + server_process.terminate() + server_process.join() + # TODO check debug endpoints From 58f6d9b393a5437b80ad358ec98a6dfc014aa554 Mon Sep 17 00:00:00 2001 From: moritzsommer Date: Wed, 29 May 2024 06:08:43 +0200 Subject: [PATCH 06/18] Adapt ci --- .github/workflows/ci.yml | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 757a553..3b95cc3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,12 +6,26 @@ on: - wip/testing jobs: - build: - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, windows-latest] + build_ubuntu: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + architecture: x64 + + - name: Install Python dependencies + run: pip install -r requirements.txt + + - name: Run Python Tests + run: python -m unittest discover + build_windows: + runs-on: windows-latest steps: - name: Checkout code uses: actions/checkout@v4 @@ -26,4 +40,4 @@ jobs: run: pip install -r requirements.txt - name: Run Python Tests - run: python -m unittest discover \ No newline at end of file + run: python -m unittest discover From 355708c172b36711ece6735f495c9ccfadaf9ad0 Mon Sep 17 00:00:00 2001 From: moritzsommer Date: Wed, 29 May 2024 06:11:32 +0200 Subject: [PATCH 07/18] Adapt ci --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3b95cc3..cd9d1c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,7 @@ jobs: build_windows: runs-on: windows-latest + needs: [build_ubuntu] # Ensures build_windows runs after build_ubuntu steps: - name: Checkout code uses: actions/checkout@v4 From 445cb91b5926fffce8c673458e286339eb7f0299 Mon Sep 17 00:00:00 2001 From: moritzsommer Date: Wed, 29 May 2024 06:17:03 +0200 Subject: [PATCH 08/18] Adapt ci, try different port for ubuntu --- .github/workflows/ci.yml | 27 ++++++--------------------- test/test_resolving_service.py | 8 ++++---- test_resources/config.ini | 2 +- 3 files changed, 11 insertions(+), 26 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd9d1c6..757a553 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,27 +6,12 @@ on: - wip/testing jobs: - build_ubuntu: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: "3.10" - architecture: x64 - - - name: Install Python dependencies - run: pip install -r requirements.txt - - - name: Run Python Tests - run: python -m unittest discover + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest] - build_windows: - runs-on: windows-latest - needs: [build_ubuntu] # Ensures build_windows runs after build_ubuntu steps: - name: Checkout code uses: actions/checkout@v4 @@ -41,4 +26,4 @@ jobs: run: pip install -r requirements.txt - name: Run Python Tests - run: python -m unittest discover + run: python -m unittest discover \ No newline at end of file diff --git a/test/test_resolving_service.py b/test/test_resolving_service.py index 0e5ad15..7df40a8 100644 --- a/test/test_resolving_service.py +++ b/test/test_resolving_service.py @@ -57,7 +57,7 @@ def test_semantic_matching_service_iri(self): semantic_id="foo://example.org:1234/over/there?name=bar#page=3", ) response = requests.get( - "http://localhost:8000/get_semantic_matching_service", + "http://localhost:8080/get_semantic_matching_service", data=sms_request.model_dump_json() ) self.assertEqual( @@ -76,7 +76,7 @@ def test_semantic_matching_service_irdi_eclass(self): semantic_id="0173-1#01-ACK323#017", ) response = requests.get( - "http://localhost:8000/get_semantic_matching_service", + "http://localhost:8080/get_semantic_matching_service", data=sms_request.model_dump_json() ) self.assertEqual( @@ -95,7 +95,7 @@ def test_semantic_matching_service_irdi_cdd(self): semantic_id="0112-1#01-ACK323#017", ) response = requests.get( - "http://localhost:8000/get_semantic_matching_service", + "http://localhost:8080/get_semantic_matching_service", data=sms_request.model_dump_json() ) self.assertEqual( @@ -114,7 +114,7 @@ def test_semantic_matching_service_fallback(self): semantic_id="nothing", ) response = requests.get( - "http://localhost:8000/get_semantic_matching_service", + "http://localhost:8080/get_semantic_matching_service", data=sms_request.model_dump_json() ) self.assertEqual( diff --git a/test_resources/config.ini b/test_resources/config.ini index 7c2ae7a..94eb25c 100644 --- a/test_resources/config.ini +++ b/test_resources/config.ini @@ -1,6 +1,6 @@ [SERVICE] endpoint=127.0.0.1 -port=8000 +port=8080 [RESOLVER] fallback_semantic_matching_service=https://example.org/fallback_semantic_matching_service From a90ba72171c2f9f4210e29c4684312567e9a5115 Mon Sep 17 00:00:00 2001 From: moritzsommer Date: Wed, 29 May 2024 06:23:32 +0200 Subject: [PATCH 09/18] Adapt ci, try different port for ubuntu --- test/test_resolving_service.py | 8 ++++---- test_resources/config.ini | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test/test_resolving_service.py b/test/test_resolving_service.py index 7df40a8..5be5a7e 100644 --- a/test/test_resolving_service.py +++ b/test/test_resolving_service.py @@ -57,7 +57,7 @@ def test_semantic_matching_service_iri(self): semantic_id="foo://example.org:1234/over/there?name=bar#page=3", ) response = requests.get( - "http://localhost:8080/get_semantic_matching_service", + "http://localhost:8125/get_semantic_matching_service", data=sms_request.model_dump_json() ) self.assertEqual( @@ -76,7 +76,7 @@ def test_semantic_matching_service_irdi_eclass(self): semantic_id="0173-1#01-ACK323#017", ) response = requests.get( - "http://localhost:8080/get_semantic_matching_service", + "http://localhost:8125/get_semantic_matching_service", data=sms_request.model_dump_json() ) self.assertEqual( @@ -95,7 +95,7 @@ def test_semantic_matching_service_irdi_cdd(self): semantic_id="0112-1#01-ACK323#017", ) response = requests.get( - "http://localhost:8080/get_semantic_matching_service", + "http://localhost:8125/get_semantic_matching_service", data=sms_request.model_dump_json() ) self.assertEqual( @@ -114,7 +114,7 @@ def test_semantic_matching_service_fallback(self): semantic_id="nothing", ) response = requests.get( - "http://localhost:8080/get_semantic_matching_service", + "http://localhost:8125/get_semantic_matching_service", data=sms_request.model_dump_json() ) self.assertEqual( diff --git a/test_resources/config.ini b/test_resources/config.ini index 94eb25c..ee3edbd 100644 --- a/test_resources/config.ini +++ b/test_resources/config.ini @@ -1,6 +1,6 @@ [SERVICE] endpoint=127.0.0.1 -port=8080 +port=8125 [RESOLVER] fallback_semantic_matching_service=https://example.org/fallback_semantic_matching_service From 4ef11e43b73430a7fdc061fd7ec39c2d295e3efe Mon Sep 17 00:00:00 2001 From: moritzsommer Date: Wed, 29 May 2024 06:38:34 +0200 Subject: [PATCH 10/18] Adapt ci, run contextmanager --- test/test_resolving_service.py | 112 +++++++++++++-------------------- 1 file changed, 43 insertions(+), 69 deletions(-) diff --git a/test/test_resolving_service.py b/test/test_resolving_service.py index 5be5a7e..26daeeb 100644 --- a/test/test_resolving_service.py +++ b/test/test_resolving_service.py @@ -11,6 +11,10 @@ from semantic_id_resolver import resolver from semantic_id_resolver.service import SemanticIdResolvingService, SMSRequest +from contextlib import contextmanager +import signal +import time + def run_server(): # Load test configuration @@ -46,84 +50,54 @@ def run_server(): uvicorn.run(app, host=str(config["SERVICE"]["ENDPOINT"]), port=int(config["SERVICE"]["PORT"]), log_level="error") -class TestSemanticMatchingService(unittest.TestCase): +@contextmanager +def run_server_context(): + server_process = multiprocessing.Process(target=run_server) + server_process.start() + try: + time.sleep(2) # Wait for the server to start + yield + finally: + server_process.terminate() + server_process.join(timeout=5) + if server_process.is_alive(): + os.kill(server_process.pid, signal.SIGKILL) + server_process.join() - def test_semantic_matching_service_iri(self): - # TODO deposit DNS record - server_process = multiprocessing.Process(target=run_server) - server_process.start() - sms_request = SMSRequest( - semantic_id="foo://example.org:1234/over/there?name=bar#page=3", - ) - response = requests.get( - "http://localhost:8125/get_semantic_matching_service", - data=sms_request.model_dump_json() - ) - self.assertEqual( - "https://example.org/fallback_semantic_matching_service", - response.json()["semantic_matching_service_endpoint"] - ) +class TestSemanticMatchingService(unittest.TestCase): - server_process.terminate() - server_process.join() + def test_semantic_matching_service_iri(self): + with run_server_context(): + sms_request = SMSRequest(semantic_id="foo://example.org:1234/over/there?name=bar#page=3") + response = requests.get("http://localhost:8125/get_semantic_matching_service", + data=sms_request.model_dump_json()) + self.assertEqual("https://example.org/fallback_semantic_matching_service", + response.json()["semantic_matching_service_endpoint"]) def test_semantic_matching_service_irdi_eclass(self): - server_process = multiprocessing.Process(target=run_server) - server_process.start() - - sms_request = SMSRequest( - semantic_id="0173-1#01-ACK323#017", - ) - response = requests.get( - "http://localhost:8125/get_semantic_matching_service", - data=sms_request.model_dump_json() - ) - self.assertEqual( - "https://example.org/eclass_semantic_matching_service", - response.json()["semantic_matching_service_endpoint"] - ) - - server_process.terminate() - server_process.join() + with run_server_context(): + sms_request = SMSRequest(semantic_id="0173-1#01-ACK323#017") + response = requests.get("http://localhost:8125/get_semantic_matching_service", + data=sms_request.model_dump_json()) + self.assertEqual("https://example.org/eclass_semantic_matching_service", + response.json()["semantic_matching_service_endpoint"]) def test_semantic_matching_service_irdi_cdd(self): - server_process = multiprocessing.Process(target=run_server) - server_process.start() - - sms_request = SMSRequest( - semantic_id="0112-1#01-ACK323#017", - ) - response = requests.get( - "http://localhost:8125/get_semantic_matching_service", - data=sms_request.model_dump_json() - ) - self.assertEqual( - "https://example.org/cdd_semantic_matching_service", - response.json()["semantic_matching_service_endpoint"] - ) - - server_process.terminate() - server_process.join() + with run_server_context(): + sms_request = SMSRequest(semantic_id="0112-1#01-ACK323#017") + response = requests.get("http://localhost:8125/get_semantic_matching_service", + data=sms_request.model_dump_json()) + self.assertEqual("https://example.org/cdd_semantic_matching_service", + response.json()["semantic_matching_service_endpoint"]) def test_semantic_matching_service_fallback(self): - server_process = multiprocessing.Process(target=run_server) - server_process.start() - - sms_request = SMSRequest( - semantic_id="nothing", - ) - response = requests.get( - "http://localhost:8125/get_semantic_matching_service", - data=sms_request.model_dump_json() - ) - self.assertEqual( - "https://example.org/fallback_semantic_matching_service", - response.json()["semantic_matching_service_endpoint"] - ) - - server_process.terminate() - server_process.join() + with run_server_context(): + sms_request = SMSRequest(semantic_id="nothing") + response = requests.get("http://localhost:8125/get_semantic_matching_service", + data=sms_request.model_dump_json()) + self.assertEqual("https://example.org/fallback_semantic_matching_service", + response.json()["semantic_matching_service_endpoint"]) # TODO check debug endpoints From b0923df07de4bee4acd428d1e57aa26048ea032e Mon Sep 17 00:00:00 2001 From: moritzsommer Date: Wed, 29 May 2024 06:45:05 +0200 Subject: [PATCH 11/18] Adapt ci oss and formatting of tests --- .github/workflows/ci.yml | 13 +++++++-- test/test_resolving_service.py | 49 +++++++++++++++++++++++----------- 2 files changed, 44 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 757a553..c650d37 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,16 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest, windows-latest] + os: + - ubuntu-latest + - ubuntu-22.04 + - ubuntu-20.04 + - windows-latest + - windows-2022 + - windows-2019 + - macos-latest + - macos-12 + - macos-11 steps: - name: Checkout code @@ -26,4 +35,4 @@ jobs: run: pip install -r requirements.txt - name: Run Python Tests - run: python -m unittest discover \ No newline at end of file + run: python -m unittest discover diff --git a/test/test_resolving_service.py b/test/test_resolving_service.py index 26daeeb..de602e8 100644 --- a/test/test_resolving_service.py +++ b/test/test_resolving_service.py @@ -68,36 +68,53 @@ def run_server_context(): class TestSemanticMatchingService(unittest.TestCase): def test_semantic_matching_service_iri(self): + # TODO deposit DNS record with run_server_context(): sms_request = SMSRequest(semantic_id="foo://example.org:1234/over/there?name=bar#page=3") - response = requests.get("http://localhost:8125/get_semantic_matching_service", - data=sms_request.model_dump_json()) - self.assertEqual("https://example.org/fallback_semantic_matching_service", - response.json()["semantic_matching_service_endpoint"]) + response = requests.get( + "http://localhost:8125/get_semantic_matching_service", + data=sms_request.model_dump_json() + ) + self.assertEqual( + "https://example.org/fallback_semantic_matching_service", + response.json()["semantic_matching_service_endpoint"] + ) def test_semantic_matching_service_irdi_eclass(self): with run_server_context(): sms_request = SMSRequest(semantic_id="0173-1#01-ACK323#017") - response = requests.get("http://localhost:8125/get_semantic_matching_service", - data=sms_request.model_dump_json()) - self.assertEqual("https://example.org/eclass_semantic_matching_service", - response.json()["semantic_matching_service_endpoint"]) + response = requests.get( + "http://localhost:8125/get_semantic_matching_service", + data=sms_request.model_dump_json() + ) + self.assertEqual( + "https://example.org/eclass_semantic_matching_service", + response.json()["semantic_matching_service_endpoint"] + ) def test_semantic_matching_service_irdi_cdd(self): with run_server_context(): sms_request = SMSRequest(semantic_id="0112-1#01-ACK323#017") - response = requests.get("http://localhost:8125/get_semantic_matching_service", - data=sms_request.model_dump_json()) - self.assertEqual("https://example.org/cdd_semantic_matching_service", - response.json()["semantic_matching_service_endpoint"]) + response = requests.get( + "http://localhost:8125/get_semantic_matching_service", + data=sms_request.model_dump_json() + ) + self.assertEqual( + "https://example.org/cdd_semantic_matching_service", + response.json()["semantic_matching_service_endpoint"] + ) def test_semantic_matching_service_fallback(self): with run_server_context(): sms_request = SMSRequest(semantic_id="nothing") - response = requests.get("http://localhost:8125/get_semantic_matching_service", - data=sms_request.model_dump_json()) - self.assertEqual("https://example.org/fallback_semantic_matching_service", - response.json()["semantic_matching_service_endpoint"]) + response = requests.get( + "http://localhost:8125/get_semantic_matching_service", + data=sms_request.model_dump_json() + ) + self.assertEqual( + "https://example.org/fallback_semantic_matching_service", + response.json()["semantic_matching_service_endpoint"] + ) # TODO check debug endpoints From 8c247c4677f0b74afa2134faed554b1e3ac47ca9 Mon Sep 17 00:00:00 2001 From: moritzsommer Date: Wed, 29 May 2024 06:46:30 +0200 Subject: [PATCH 12/18] Remove ci macos --- .github/workflows/ci.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c650d37..1750d39 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,9 +17,6 @@ jobs: - windows-latest - windows-2022 - windows-2019 - - macos-latest - - macos-12 - - macos-11 steps: - name: Checkout code From 17e59dac0b5413694c971e57ec9158d631142aa2 Mon Sep 17 00:00:00 2001 From: moritzsommer Date: Sun, 7 Jul 2024 01:19:07 +0200 Subject: [PATCH 13/18] Add irdi parser --- semantic_id_resolver/resolver.py | 19 +++++++++++-------- test/test_resolving_service.py | 4 ++-- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/semantic_id_resolver/resolver.py b/semantic_id_resolver/resolver.py index b0fc090..010c3e0 100644 --- a/semantic_id_resolver/resolver.py +++ b/semantic_id_resolver/resolver.py @@ -4,6 +4,7 @@ import json import dns.resolver +from parser.irdi_parser import IRDIParser class DebugSemanticMatchingServiceEndpoints: @@ -33,18 +34,20 @@ def get_debug_endpoint(self, semantic_id: str) -> Optional[str]: def is_iri_not_irdi(semantic_id: str) -> Optional[bool]: """ - :return: `True`, if `semantic_id` is a IRI, False if it is an `IRDI`, None for neither + :return: `True`, if `semantic_id` is an IRI, False if it is an IRDI, None for neither """ + # Check IRDI + try: + IRDIParser().parse(semantic_id) + return False + except ValueError: + pass + # Check IRI parsed_url = urlparse(semantic_id) - # Check if the scheme is present, which indicates it's a URI if parsed_url.scheme: return True - # TODO IRDI parser - elif "#" in parsed_url.fragment: - return False - # If neither condition is met, return None - else: - return None + # Not IRDI or IRI + return None class IRDISources(enum.Enum): diff --git a/test/test_resolving_service.py b/test/test_resolving_service.py index de602e8..a1c72be 100644 --- a/test/test_resolving_service.py +++ b/test/test_resolving_service.py @@ -82,7 +82,7 @@ def test_semantic_matching_service_iri(self): def test_semantic_matching_service_irdi_eclass(self): with run_server_context(): - sms_request = SMSRequest(semantic_id="0173-1#01-ACK323#017") + sms_request = SMSRequest(semantic_id="0173-0001#01-ACK323#7") response = requests.get( "http://localhost:8125/get_semantic_matching_service", data=sms_request.model_dump_json() @@ -94,7 +94,7 @@ def test_semantic_matching_service_irdi_eclass(self): def test_semantic_matching_service_irdi_cdd(self): with run_server_context(): - sms_request = SMSRequest(semantic_id="0112-1#01-ACK323#017") + sms_request = SMSRequest(semantic_id="0112-0001#01-ACK323#7") response = requests.get( "http://localhost:8125/get_semantic_matching_service", data=sms_request.model_dump_json() From 908e364af115ded106bcdb8ed2a2002f72c63a99 Mon Sep 17 00:00:00 2001 From: moritzsommer Date: Sun, 7 Jul 2024 04:15:45 +0200 Subject: [PATCH 14/18] Add mock dns --- semantic_id_resolver/resolver.py | 1 + test/test_resolving_service.py | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/semantic_id_resolver/resolver.py b/semantic_id_resolver/resolver.py index 010c3e0..a4fe76a 100644 --- a/semantic_id_resolver/resolver.py +++ b/semantic_id_resolver/resolver.py @@ -71,6 +71,7 @@ def _iri_find_semantic_matching_service(semantic_id: str) -> Optional[str]: try: semantic_matcher_endpoint = semantic_matcher_record.split(": ")[-1] return semantic_matcher_endpoint + # ToDo What happens if we have several services here? except Exception as e: print(f"Cannot parse TXT record {semantic_matcher_record} for {domain}: {e}") return None diff --git a/test/test_resolving_service.py b/test/test_resolving_service.py index a1c72be..1151c6c 100644 --- a/test/test_resolving_service.py +++ b/test/test_resolving_service.py @@ -2,6 +2,8 @@ import configparser from typing import Dict import multiprocessing + +import dns import requests import unittest @@ -45,6 +47,17 @@ def run_server(): semantic_id_resolver=mock_resolver ) + # Mock TXT record + class MockTXTRecord: + def __init__(self, strings): + self.strings = strings + + # Mock DNS resolving manually, mock for unittest does not work in this context + def mock_dns_resolver_resolve(qname, rdtype): + return [MockTXTRecord([b"semantic_matcher: https://example.org/iri_semantic_matching_service"])] + dns.resolver.resolve = mock_dns_resolver_resolve + + # Run server app = FastAPI() app.include_router(semantic_id_resolver_service.router) uvicorn.run(app, host=str(config["SERVICE"]["ENDPOINT"]), port=int(config["SERVICE"]["PORT"]), log_level="error") @@ -68,7 +81,6 @@ def run_server_context(): class TestSemanticMatchingService(unittest.TestCase): def test_semantic_matching_service_iri(self): - # TODO deposit DNS record with run_server_context(): sms_request = SMSRequest(semantic_id="foo://example.org:1234/over/there?name=bar#page=3") response = requests.get( @@ -76,7 +88,7 @@ def test_semantic_matching_service_iri(self): data=sms_request.model_dump_json() ) self.assertEqual( - "https://example.org/fallback_semantic_matching_service", + "https://example.org/iri_semantic_matching_service", response.json()["semantic_matching_service_endpoint"] ) From cdac2c039d31f656dd88a43422693cc575136aa9 Mon Sep 17 00:00:00 2001 From: moritzsommer Date: Sun, 7 Jul 2024 04:41:45 +0200 Subject: [PATCH 15/18] Add debug test --- semantic_id_resolver/resolver.py | 1 - semantic_id_resolver/service.py | 3 +-- test/test_resolving_service.py | 12 +++++++++++- test_resources/config.ini | 2 +- test_resources/debug_endpoints.json | 3 +-- 5 files changed, 14 insertions(+), 7 deletions(-) diff --git a/semantic_id_resolver/resolver.py b/semantic_id_resolver/resolver.py index a4fe76a..010c3e0 100644 --- a/semantic_id_resolver/resolver.py +++ b/semantic_id_resolver/resolver.py @@ -71,7 +71,6 @@ def _iri_find_semantic_matching_service(semantic_id: str) -> Optional[str]: try: semantic_matcher_endpoint = semantic_matcher_record.split(": ")[-1] return semantic_matcher_endpoint - # ToDo What happens if we have several services here? except Exception as e: print(f"Cannot parse TXT record {semantic_matcher_record} for {domain}: {e}") return None diff --git a/semantic_id_resolver/service.py b/semantic_id_resolver/service.py index 16289d4..07625bd 100644 --- a/semantic_id_resolver/service.py +++ b/semantic_id_resolver/service.py @@ -57,7 +57,7 @@ def get_semantic_matching_service( endpoint = found_endpoint return SMSResponse( semantic_matching_service_endpoint=endpoint, - meta_information={} # TODO metainformation + meta_information={} ) @@ -97,5 +97,4 @@ def get_semantic_matching_service( APP.include_router( SEMANTIC_ID_RESOLVING_SERVICE.router ) - # TODO read host from config uvicorn.run(APP, host="127.0.0.1", port=int(config["SERVICE"]["PORT"])) diff --git a/test/test_resolving_service.py b/test/test_resolving_service.py index 1151c6c..eba6220 100644 --- a/test/test_resolving_service.py +++ b/test/test_resolving_service.py @@ -128,7 +128,17 @@ def test_semantic_matching_service_fallback(self): response.json()["semantic_matching_service_endpoint"] ) - # TODO check debug endpoints + def test_semantic_matching_service_debug(self): + with run_server_context(): + sms_request = SMSRequest(semantic_id="https://example.org/semanticIDone") + response = requests.get( + "http://localhost:8125/get_semantic_matching_service", + data=sms_request.model_dump_json() + ) + self.assertEqual( + "https://example.org/debug_semantic_matching_service", + response.json()["semantic_matching_service_endpoint"] + ) if __name__ == '__main__': diff --git a/test_resources/config.ini b/test_resources/config.ini index ee3edbd..4c4f990 100644 --- a/test_resources/config.ini +++ b/test_resources/config.ini @@ -6,4 +6,4 @@ port=8125 fallback_semantic_matching_service=https://example.org/fallback_semantic_matching_service eclass_semantic_matching_service=https://example.org/eclass_semantic_matching_service cdd_semantic_matching_service=https://example.org/cdd_semantic_matching_service -debug_semantic_matching_service_endpoints=../debug_endpoints.json +debug_semantic_matching_service_endpoints=../test_resources/debug_endpoints.json diff --git a/test_resources/debug_endpoints.json b/test_resources/debug_endpoints.json index c5395a9..85573cb 100644 --- a/test_resources/debug_endpoints.json +++ b/test_resources/debug_endpoints.json @@ -1,4 +1,3 @@ { - "https://example.org/semanticIDone": "http://localhost:1234/semantic_matching_service", - "https://example.org/semanticIDtwo": "http://localhost:1234/semantic_matching_service" + "https://example.org/semanticIDone": "https://example.org/debug_semantic_matching_service" } From e31a6c5c18ef62561d63d251765973ab03271b09 Mon Sep 17 00:00:00 2001 From: moritzsommer Date: Sun, 7 Jul 2024 04:44:26 +0200 Subject: [PATCH 16/18] Adapt requirements --- requirements.txt | Bin 99 -> 1628 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/requirements.txt b/requirements.txt index aa9f52e8e00df2eb55680771e28f0439aad200d7..49c7a46f750781c67c779e4d769870e8744e3137 100644 GIT binary patch literal 1628 zcmZ8h!ET#S5ZrU6ehN{*Nz)#3$Tdo&NImsr5rYj*3<;Rp>BqO7*vHcuG_yW7gaN zL2Cm}m`!|St?V@_^~lT-)oeJ|)SsB&c}{}r(`OA> zaDsu3J16ga&RCx5D<<3sZORksZQ$k7D@xxfM@q0Fx1Ek;Ki!`9i#J)uqOT{qT7a-!HqfM|<#k^xdR=hfeRPB5V`-g)1h8ozJNn(a3{= zy~lo+z`)I+7W~%iPPjRBxu{2!-zj=k-!k%&wp2yCN=NW3MgHVYoD1ItA>kJ7yjQ3; zF^R9J9IkWfAlE1=j7wBMdJlf3^equ{dMI_~(rq|%BH74^5hY$Se@2%;B<;y*fP&i5 o4(x+Z23=x9517^-?y7t)GFN=vc$GTqa~ahNKv}CiK8=Gfs&$q n(dC-v#sR`7DxL2<1YrdfHM}hl)9eKYP~cWL=V#D<0D}Ah1%V!O From e38f37e29197d9f685baa8c61ae3116cebf452a2 Mon Sep 17 00:00:00 2001 From: moritzsommer Date: Sun, 7 Jul 2024 05:08:52 +0200 Subject: [PATCH 17/18] Adapt paths for linux --- config.ini.default | 2 +- semantic_id_resolver/resolver.py | 5 ++++- test_resources/config.ini | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/config.ini.default b/config.ini.default index 87f96bf..96a5de4 100644 --- a/config.ini.default +++ b/config.ini.default @@ -6,4 +6,4 @@ port=8125 fallback_semantic_matching_service=https://example.org/semantic_matching_service eclass_semantic_matching_service=https://example.org/semantic_matching_service cdd_semantic_matching_service=https://example.org/semantic_matching_service -debug_semantic_matching_service_endpoints=../debug_endpoints.json +debug_semantic_matching_service_endpoints=debug_endpoints.json diff --git a/semantic_id_resolver/resolver.py b/semantic_id_resolver/resolver.py index 010c3e0..bb07c8d 100644 --- a/semantic_id_resolver/resolver.py +++ b/semantic_id_resolver/resolver.py @@ -1,4 +1,5 @@ import enum +from pathlib import Path from typing import Optional, Dict from urllib.parse import urlparse import json @@ -24,7 +25,9 @@ def __init__(self, debug_endpoints: Dict[str, str]): @classmethod def from_file(cls, filename: str) -> "DebugSemanticMatchingServiceEndpoints": - with open(filename, "r") as file: + base_dir = str(Path(__file__).resolve().parent.parent) + resource_path = base_dir + "/" + filename + with open(resource_path, "r") as file: debug_endpoints = json.load(file) return DebugSemanticMatchingServiceEndpoints(debug_endpoints) diff --git a/test_resources/config.ini b/test_resources/config.ini index 4c4f990..d546395 100644 --- a/test_resources/config.ini +++ b/test_resources/config.ini @@ -6,4 +6,4 @@ port=8125 fallback_semantic_matching_service=https://example.org/fallback_semantic_matching_service eclass_semantic_matching_service=https://example.org/eclass_semantic_matching_service cdd_semantic_matching_service=https://example.org/cdd_semantic_matching_service -debug_semantic_matching_service_endpoints=../test_resources/debug_endpoints.json +debug_semantic_matching_service_endpoints=test_resources/debug_endpoints.json From 390966c5079915f5957d869e45581d8d4be2dd15 Mon Sep 17 00:00:00 2001 From: moritzsommer Date: Thu, 11 Jul 2024 11:07:28 +0200 Subject: [PATCH 18/18] Adapt requirements --- requirements.txt | Bin 1628 -> 210 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/requirements.txt b/requirements.txt index 49c7a46f750781c67c779e4d769870e8744e3137..b4d8e99b6349702dc417cec26d21a1d1f940e9aa 100644 GIT binary patch literal 210 zcmXwzK@Ng26hzKIkV}JU)CiO2CrTscPVWzV@mMc)~Aano# literal 1628 zcmZ8h!ET#S5ZrU6ehN{*Nz)#3$Tdo&NImsr5rYj*3<;Rp>BqO7*vHcuG_yW7gaN zL2Cm}m`!|St?V@_^~lT-)oeJ|)SsB&c}{}r(`OA> zaDsu3J16ga&RCx5D<<3sZORksZQ$k7D@xxfM@q0Fx1Ek;Ki!`9i#J)uqOT{qT7a-!HqfM|<#k^xdR=hfeRPB5V`-g)1h8ozJNn(a3{= zy~lo+z`)I+7W~%iPPjRBxu{2!-zj=k-!k%&wp2yCN=NW3MgHVYoD1ItA>kJ7yjQ3; zF^R9J9IkWfAlE1=j7wBMdJlf3^equ{dMI_~(rq|%BH74^5hY$Se@2%;B<;y*fP&i5 o4(x+Z23=x9517^-?y7t)