From 994a705e4ea7d161fdf321b1ef4148181a68e156 Mon Sep 17 00:00:00 2001 From: Unni Kohonen Date: Wed, 6 Sep 2023 15:06:45 +0300 Subject: [PATCH 1/7] Add reconciliation api --- annif/openapi/annif.yaml | 182 +++++++++++++++++++++++++++++++++++++++ annif/rest.py | 60 +++++++++++++ tests/test_rest.py | 28 ++++++ 3 files changed, 270 insertions(+) diff --git a/annif/openapi/annif.yaml b/annif/openapi/annif.yaml index c5143313d..4a8c0de56 100644 --- a/annif/openapi/annif.yaml +++ b/annif/openapi/annif.yaml @@ -180,6 +180,93 @@ paths: "503": $ref: '#/components/responses/ServiceUnavailable' x-codegen-request-body-name: documents + /projects/{project_id}/reconcile: + get: + tags: + - Reconciliation + summary: reconcile against a project + operationId: annif.rest.reconcile_metadata + parameters: + - $ref: '#/components/parameters/project_id' + # queries parameter doesn't work for some reason + - in: query + name: queries + schema: + type: object + additionalProperties: + type: object + required: + - query + properties: + query: + type: string + description: query string + example: + {"q0": {"query": "query"}} + required: false + responses: + "200": + description: successful operation + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/ReconcileMetadata' + - $ref: '#/components/schemas/Reconcile' + "404": + $ref: '#/components/responses/NotFound' + post: + tags: + - Reconciliation + summary: reconcole against a project + operationId: annif.rest.reconcile + parameters: + - $ref: '#components/parameters/project_id' + requestBody: + content: + application/x-www-form-urlencoded: + encoding: + queries: + contentType: application/json + schema: + type: object + required: + - queries + properties: + queries: + type: object + additionalProperties: + type: object + required: + - query + properties: + query: + type: string + description: query string + limit: + type: integer + description: maximum number of results to return + example: + { + "q0": { + "query": "cat", + "limit": 10 + }, + "q1": { + "query": "dog", + "limit": 10 + } + } + required: true + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Reconcile' + "404": + $ref: '#/components/responses/NotFound' components: schemas: ApiInfo: @@ -354,6 +441,101 @@ components: An absolute URI that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced. format: uri + ReconcileMetadata: + required: + - name + - defaultTypes + - view + - identifierSpace + - schemaSpace + - versions + type: object + properties: + name: + type: string + example: Annif Reconciliation Service + identifierSpace: + type: string + example: "" + schemaSpace: + type: string + example: "" + defaultTypes: + type: array + items: + type: object + required: + - id + - name + properties: + id: + type: string + example: type_id + name: + type: string + example: type_name + view: + type: object + required: + - url + properties: + url: + type: string + example: '{{id}}' + versions: + type: array + items: + type: string + example: 0.2 + description: Reconciliation service information + Reconcile: + type: object + additionalProperties: + type: object + required: + - result + properties: + result: + type: array + items: + type: object + required: + - id + - name + - score + - match + properties: + id: + type: string + name: + type: string + score: + type: number + match: + type: boolean + example: + { + "q0": { + "result": [ + { + "id": "id0", + "name": "name0", + "score": 0.5, + "match": true + } + ] + }, + "q1": { + "result": [ + { + "id": "id1", + "name": "name1", + "score": 0.5, + "match": false + } + ] + } + } parameters: project_id: name: project_id diff --git a/annif/rest.py b/annif/rest.py index f848117c8..087f60f90 100644 --- a/annif/rest.py +++ b/annif/rest.py @@ -214,3 +214,63 @@ def learn( return server_error(err) return None, 204 + + +def _reconcile(project_id: str, query: dict[str, Any]) -> dict[str, Any]: + document = [{"text": query["query"]}] + parameters = {"limit": query["limit"]} if "limit" in query else {} + result = _suggest(project_id, document, parameters) + + if _is_error(result): + return result + + results = [ + { + "id": res["uri"], + "name": res["label"], + "score": res["score"], + "match": res["label"] == query["query"], + } + for res in result[0]["results"] + ] + return results + + +def reconcile_metadata( + project_id: str, **query_parameters +) -> ConnexionResponse | dict[str, Any]: + """return service manifest or reconcile against a project and return a dict + with results formatted according to OpenAPI spec""" + + try: + project = annif.registry.get_project(project_id, min_access=Access.hidden) + except ValueError: + return project_not_found_error(project_id) + + if not query_parameters: + return { + "versions": ["0.2"], + "name": "Annif Reconciliation Service for " + project.name, + "identifierSpace": "", + "schemaSpace": "", + "view": {"url": "{{id}}"}, + } + else: + return {} + + +def reconcile( + project_id: str, body: dict[str, Any] +) -> ConnexionResponse | dict[str, Any]: + """reconcile against a project and return a dict with results + formatted according to OpenAPI spec""" + + queries = body["queries"] + results = {} + for key, query in queries.items(): + data = _reconcile(project_id, query) + if _is_error(data): + return data + results[key] = {"result": data} + + return results diff --git a/tests/test_rest.py b/tests/test_rest.py index e56a24b21..94c0d944a 100644 --- a/tests/test_rest.py +++ b/tests/test_rest.py @@ -233,3 +233,31 @@ def test_rest_learn_not_supported(app): with app.app_context(): result = annif.rest.learn("tfidf-fi", []) assert result.status_code == 503 + + +def test_rest_reconcile_metadata(app): + with app.app_context(): + results = annif.rest.reconcile_metadata("dummy-fi") + assert results["name"] == "Annif Reconciliation Service for Dummy Finnish" + + +def test_rest_reocncile_metadata_nonexistent(app): + with app.app_context(): + result = annif.rest.reconcile_metadata("nonexistent") + assert result.status_code == 404 + + +def test_rest_reconcile(app): + with app.app_context(): + results = annif.rest.reconcile( + "dummy-fi", {"queries": {"q0": {"query": "example text"}}} + ) + assert "result" in results["q0"] + + +def test_test_reconcile_nonexistent(app): + with app.app_context(): + result = annif.rest.reconcile( + "nonexistent", {"queries": {"q0": {"query": "example text"}}} + ) + assert result.status_code == 404 From de054db16f9052919a42491aee455580e0f389b8 Mon Sep 17 00:00:00 2001 From: Unni Kohonen Date: Fri, 8 Sep 2023 14:11:37 +0300 Subject: [PATCH 2/7] Fix queries parameter in reconciliation GET request --- annif/openapi/annif.yaml | 191 ++++++++++++++++++++++++++------------- annif/rest.py | 14 ++- tests/test_rest.py | 18 +++- 3 files changed, 156 insertions(+), 67 deletions(-) diff --git a/annif/openapi/annif.yaml b/annif/openapi/annif.yaml index 4a8c0de56..c07e7d038 100644 --- a/annif/openapi/annif.yaml +++ b/annif/openapi/annif.yaml @@ -184,26 +184,38 @@ paths: get: tags: - Reconciliation - summary: reconcile against a project + summary: get reconciliation service manifest or reconcile against a project operationId: annif.rest.reconcile_metadata parameters: - $ref: '#/components/parameters/project_id' - # queries parameter doesn't work for some reason - in: query + description: A call to the reconciliation service API name: queries + required: false schema: - type: object + type: string additionalProperties: type: object required: - - query + - query properties: query: type: string - description: query string - example: - {"q0": {"query": "query"}} - required: false + description: Query string to search for + limit: + type: integer + description: Maximum number of results to return + example: + '{ + "q0": { + "query": "example query", + "limit": 10 + }, + "q1": { + "query": "another example", + "limit": 15 + } + }' responses: "200": description: successful operation @@ -212,7 +224,53 @@ paths: schema: oneOf: - $ref: '#/components/schemas/ReconcileMetadata' - - $ref: '#/components/schemas/Reconcile' + - $ref: '#/components/schemas/ReconciliationResult' + examples: + ReconcileMetadata: + summary: Reconciliation service manifest + value: + { + "defaultTypes": [ + { + "id": "default-type", + "name": "Default type" + } + ], + "identifierSpace": "", + "name": "Annif Reconciliation Service for Dummy Finnish", + "schemaSpace": "http://www.w3.org/2004/02/skos/core#Concept", + "versions": [ + "0.2" + ], + "view": { + "url": "{{id}}" + } + } + ReconciliationResult: + summary: Reconciliation result + value: + { + "q0": { + "result": [ + { + "id": "example-id", + "name": "example name", + "score": 0.5, + "match": true + } + ] + }, + "q1": { + "result": [ + { + "id": "another-id", + "name": "another name", + "score": 0.5, + "match": false + } + ] + } + } "404": $ref: '#/components/responses/NotFound' post: @@ -235,6 +293,7 @@ paths: properties: queries: type: object + description: A call to the reconciliation service API additionalProperties: type: object required: @@ -242,19 +301,19 @@ paths: properties: query: type: string - description: query string + description: Query string to search for limit: type: integer - description: maximum number of results to return + description: Maximum number of results to return example: { "q0": { - "query": "cat", + "query": "example query", "limit": 10 }, "q1": { - "query": "dog", - "limit": 10 + "query": "another example", + "limit": 15 } } required: true @@ -264,7 +323,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Reconcile' + $ref: '#/components/schemas/ReconciliationResult' "404": $ref: '#/components/responses/NotFound' components: @@ -401,46 +460,6 @@ components: type: string example: Vulpes vulpes description: A document with attached, known good subjects - Problem: - type: object - properties: - type: - type: string - description: | - An absolute URI that identifies the problem type. When dereferenced, - it SHOULD provide human-readable documentation for the problem type - (e.g., using HTML). - format: uri - example: https://zalando.github.io/problem/constraint-violation - default: about:blank - title: - type: string - description: | - A short, summary of the problem type. Written in english and readable - for engineers (usually not suited for non technical stakeholders and - not localized); example: Service Unavailable - status: - maximum: 600 - exclusiveMaximum: true - minimum: 100 - type: integer - description: | - The HTTP status code generated by the origin server for this occurrence - of the problem. - format: int32 - example: 503 - detail: - type: string - description: | - A human readable explanation specific to this occurrence of the - problem. - example: Connection to database timed out - instance: - type: string - description: | - An absolute URI that identifies the specific occurrence of the problem. - It may or may not yield further information if dereferenced. - format: uri ReconcileMetadata: required: - name @@ -459,7 +478,7 @@ components: example: "" schemaSpace: type: string - example: "" + example: "http://www.w3.org/2004/02/skos/core#Concept" defaultTypes: type: array items: @@ -470,10 +489,10 @@ components: properties: id: type: string - example: type_id + example: type-id name: type: string - example: type_name + example: type name view: type: object required: @@ -481,14 +500,14 @@ components: properties: url: type: string - example: '{{id}}' + example: "{{id}}" versions: type: array items: type: string example: 0.2 description: Reconciliation service information - Reconcile: + ReconciliationResult: type: object additionalProperties: type: object @@ -507,19 +526,23 @@ components: properties: id: type: string + example: example-id name: type: string + example: example name score: type: number + example: 0.5 match: type: boolean + example: true example: { "q0": { "result": [ { - "id": "id0", - "name": "name0", + "id": "example-id", + "name": "example name", "score": 0.5, "match": true } @@ -528,14 +551,54 @@ components: "q1": { "result": [ { - "id": "id1", - "name": "name1", + "id": "another-id", + "name": "another name", "score": 0.5, "match": false } ] } } + Problem: + type: object + properties: + type: + type: string + description: | + An absolute URI that identifies the problem type. When dereferenced, + it SHOULD provide human-readable documentation for the problem type + (e.g., using HTML). + format: uri + example: https://zalando.github.io/problem/constraint-violation + default: about:blank + title: + type: string + description: | + A short, summary of the problem type. Written in english and readable + for engineers (usually not suited for non technical stakeholders and + not localized); example: Service Unavailable + status: + maximum: 600 + exclusiveMaximum: true + minimum: 100 + type: integer + description: | + The HTTP status code generated by the origin server for this occurrence + of the problem. + format: int32 + example: 503 + detail: + type: string + description: | + A human readable explanation specific to this occurrence of the + problem. + example: Connection to database timed out + instance: + type: string + description: | + An absolute URI that identifies the specific occurrence of the problem. + It may or may not yield further information if dereferenced. + format: uri parameters: project_id: name: project_id diff --git a/annif/rest.py b/annif/rest.py index 087f60f90..6edbf2960 100644 --- a/annif/rest.py +++ b/annif/rest.py @@ -3,6 +3,7 @@ from __future__ import annotations import importlib +import json from typing import TYPE_CHECKING, Any import connexion @@ -252,11 +253,20 @@ def reconcile_metadata( "versions": ["0.2"], "name": "Annif Reconciliation Service for " + project.name, "identifierSpace": "", - "schemaSpace": "", + "schemaSpace": "http://www.w3.org/2004/02/skos/core#Concept", "view": {"url": "{{id}}"}, + "defaultTypes": [{"id": "default-type", "name": "Default type"}], } else: - return {} + queries = json.loads(query_parameters["queries"]) + results = {} + for key, query in queries.items(): + data = _reconcile(project_id, query) + if _is_error(data): + return data + results[key] = {"result": data} + + return results def reconcile( diff --git a/tests/test_rest.py b/tests/test_rest.py index 94c0d944a..104999cc6 100644 --- a/tests/test_rest.py +++ b/tests/test_rest.py @@ -247,6 +247,22 @@ def test_rest_reocncile_metadata_nonexistent(app): assert result.status_code == 404 +def test_rest_reconcile_metadata_queries(app): + with app.app_context(): + results = annif.rest.reconcile_metadata( + "dummy-fi", queries='{"q0": {"query": "example text"}}' + ) + assert "result" in results["q0"] + + +def test_rest_reconcile_metadata_queries_nonexistent(app): + with app.app_context(): + result = annif.rest.reconcile_metadata( + "nonexistent", queries='{"q0": {"query": "example text"}}' + ) + assert result.status_code == 404 + + def test_rest_reconcile(app): with app.app_context(): results = annif.rest.reconcile( @@ -255,7 +271,7 @@ def test_rest_reconcile(app): assert "result" in results["q0"] -def test_test_reconcile_nonexistent(app): +def test_rest_reconcile_nonexistent(app): with app.app_context(): result = annif.rest.reconcile( "nonexistent", {"queries": {"q0": {"query": "example text"}}} From 9a1301464c041d0a68264a3ef6476f6ea0051249 Mon Sep 17 00:00:00 2001 From: Unni Kohonen Date: Wed, 18 Oct 2023 15:58:46 +0300 Subject: [PATCH 3/7] Change naming --- annif/openapi/annif.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/annif/openapi/annif.yaml b/annif/openapi/annif.yaml index c07e7d038..317922298 100644 --- a/annif/openapi/annif.yaml +++ b/annif/openapi/annif.yaml @@ -223,10 +223,10 @@ paths: application/json: schema: oneOf: - - $ref: '#/components/schemas/ReconcileMetadata' + - $ref: '#/components/schemas/ReconcileServiceManifest' - $ref: '#/components/schemas/ReconciliationResult' examples: - ReconcileMetadata: + ReconcileServiceManifest: summary: Reconciliation service manifest value: { @@ -460,7 +460,7 @@ components: type: string example: Vulpes vulpes description: A document with attached, known good subjects - ReconcileMetadata: + ReconcileServiceManifest: required: - name - defaultTypes From 4fb1bd3ae3c6128c4773ef3b89c8932cc8091da9 Mon Sep 17 00:00:00 2001 From: Unni Kohonen Date: Wed, 18 Oct 2023 15:59:02 +0300 Subject: [PATCH 4/7] Fix type --- annif/rest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/annif/rest.py b/annif/rest.py index 6edbf2960..cfb3d8545 100644 --- a/annif/rest.py +++ b/annif/rest.py @@ -217,7 +217,7 @@ def learn( return None, 204 -def _reconcile(project_id: str, query: dict[str, Any]) -> dict[str, Any]: +def _reconcile(project_id: str, query: dict[str, Any]) -> list[dict[str, Any]]: document = [{"text": query["query"]}] parameters = {"limit": query["limit"]} if "limit" in query else {} result = _suggest(project_id, document, parameters) From 57c2893372b76170007267c5ac633c7c27502e1c Mon Sep 17 00:00:00 2001 From: Unni Kohonen Date: Wed, 18 Oct 2023 15:59:24 +0300 Subject: [PATCH 5/7] Add openapi tests --- tests/test_openapi.py | 47 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/tests/test_openapi.py b/tests/test_openapi.py index 26e33e4ea..de1189515 100644 --- a/tests/test_openapi.py +++ b/tests/test_openapi.py @@ -1,5 +1,7 @@ """Unit tests for Annif REST API / OpenAPI spec""" +import json + import pytest import schemathesis from hypothesis import settings @@ -113,3 +115,48 @@ def test_openapi_learn_novocab(app_client): data = [] req = app_client.post("http://localhost:8000/v1/projects/novocab/learn", json=data) assert req.status_code == 503 + + +def test_openapi_reconcile_metadata(app_client): + req = app_client.get("http://localhost:8000/v1/projects/dummy-fi/reconcile") + assert req.status_code == 200 + assert "name" in req.get_json() + + +def test_openapi_reconcile_metadata_nonexistent(app_client): + req = app_client.get("http://localhost:8000/v1/projects/nonexistent/reconcile") + assert req.status_code == 404 + + +def test_openapi_reconcile_metadata_queries(app_client): + req = app_client.get( + 'http://localhost:8000/v1/projects/dummy-fi/reconcile?queries=\ + {"q0": {"query": "example text"}}' + ) + assert req.status_code == 200 + assert "result" in req.get_json()["q0"] + + +def test_openapi_reconcile_metadata_queries_nonexistent(app_client): + req = app_client.get( + 'http://localhost:8000/v1/projects/nonexistent/reconcile?queries=\ + {"q0": {"query": "example text"}}' + ) + assert req.status_code == 404 + + +def test_openapi_reconcile(app_client): + data = {"queries": json.dumps({"q0": {"query": "example text"}})} + req = app_client.post( + "http://localhost:8000/v1/projects/dummy-fi/reconcile", data=data + ) + assert req.status_code == 200 + assert "result" in req.get_json()["q0"] + + +def test_openapi_reconcile_nonexistent(app_client): + data = {"queries": json.dumps({"q0": {"query": "example text"}})} + req = app_client.post( + "http://localhost:8000/v1/projects/nonexistent/reconcile", data=data + ) + assert req.status_code == 404 From eaec714c8d11e1a3d92ae2edd1381cb04b08fdcf Mon Sep 17 00:00:00 2001 From: Unni Kohonen Date: Tue, 24 Oct 2023 15:41:31 +0300 Subject: [PATCH 6/7] Add suggest service for reconciliation API --- annif/openapi/annif.yaml | 57 +++++++++++++++++++++++++++++++++++++++- annif/rest.py | 26 ++++++++++++++++++ tests/test_openapi.py | 15 +++++++++++ tests/test_rest.py | 12 +++++++++ 4 files changed, 109 insertions(+), 1 deletion(-) diff --git a/annif/openapi/annif.yaml b/annif/openapi/annif.yaml index 317922298..531b0657d 100644 --- a/annif/openapi/annif.yaml +++ b/annif/openapi/annif.yaml @@ -242,6 +242,12 @@ paths: "versions": [ "0.2" ], + "suggest": { + "entity": { + "service_path": "/suggest/entity", + "service_url": "/v1/projects/dummy-fi/reconcile" + } + }, "view": { "url": "{{id}}" } @@ -276,7 +282,7 @@ paths: post: tags: - Reconciliation - summary: reconcole against a project + summary: reconcile against a project operationId: annif.rest.reconcile parameters: - $ref: '#components/parameters/project_id' @@ -326,6 +332,36 @@ paths: $ref: '#/components/schemas/ReconciliationResult' "404": $ref: '#/components/responses/NotFound' + /projects/{project_id}/reconcile/suggest/entity: + get: + tags: + - Reconciliation + summary: Entity auto-complete endpoint for the reconciliation service + operationId: annif.rest.reconcile_suggest + parameters: + - $ref: '#components/parameters/project_id' + - in: query + description: string to get suggestions for + name: prefix + required: true + schema: + type: string + example: example query + - in: query + description: number of suggestions to skip + name: cursor + required: false + schema: + type: integer + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ReconcileSuggestResult' + "404": + $ref: '#/components/responses/NotFound' components: schemas: ApiInfo: @@ -559,6 +595,25 @@ components: ] } } + ReconcileSuggestResult: + type: object + required: + - result + properties: + result: + type: array + items: + type: object + required: + - name + - id + properties: + name: + type: string + example: example name + id: + type: string + example: example-id Problem: type: object properties: diff --git a/annif/rest.py b/annif/rest.py index cfb3d8545..bc57689ee 100644 --- a/annif/rest.py +++ b/annif/rest.py @@ -256,6 +256,14 @@ def reconcile_metadata( "schemaSpace": "http://www.w3.org/2004/02/skos/core#Concept", "view": {"url": "{{id}}"}, "defaultTypes": [{"id": "default-type", "name": "Default type"}], + "suggest": { + "entity": { + "service_path": "/suggest/entity", + "service_url": "http://localhost:5000/v1/projects/" + + project_id + + "/reconcile", # change to actual host url (how?) + } + }, } else: queries = json.loads(query_parameters["queries"]) @@ -284,3 +292,21 @@ def reconcile( results[key] = {"result": data} return results + + +def reconcile_suggest( + project_id: str, **query_parameters +) -> ConnexionResponse | dict[str, Any]: + """suggest results for the given search term and return a dict with results + formatted according to OpenAPI spec""" + + prefix = query_parameters.get("prefix") + cursor = query_parameters.get("cursor") if query_parameters.get("cursor") else 0 + limit = cursor + 10 + + result = _suggest(project_id, [{"text": prefix}], {"limit": limit}) + if _is_error(result): + return result + + results = [{"id": res["uri"], "name": res["label"]} for res in result[0]["results"]] + return {"result": results[cursor:]} diff --git a/tests/test_openapi.py b/tests/test_openapi.py index de1189515..b1dfa5399 100644 --- a/tests/test_openapi.py +++ b/tests/test_openapi.py @@ -160,3 +160,18 @@ def test_openapi_reconcile_nonexistent(app_client): "http://localhost:8000/v1/projects/nonexistent/reconcile", data=data ) assert req.status_code == 404 + + +def test_openapi_reconcile_suggest(app_client): + req = app_client.get( + "http://localhost:8000/v1/projects/dummy-fi/reconcile/suggest/entity?prefix=example" + ) + assert req.status_code == 200 + assert "result" in req.get_json() + + +def test_openapi_reconcile_suggest_nonexistent(app_client): + req = app_client.get( + "http://localhost:8000/v1/projects/nonexistent/reconcile/suggest/entity?prefix=example" + ) + assert req.status_code == 404 diff --git a/tests/test_rest.py b/tests/test_rest.py index 104999cc6..51c698137 100644 --- a/tests/test_rest.py +++ b/tests/test_rest.py @@ -277,3 +277,15 @@ def test_rest_reconcile_nonexistent(app): "nonexistent", {"queries": {"q0": {"query": "example text"}}} ) assert result.status_code == 404 + + +def test_rest_reconcile_suggest(app): + with app.app_context(): + results = annif.rest.reconcile_suggest("dummy-fi", prefix="example text") + assert "result" in results + + +def test_rest_reconcile_nonexistent(app): + with app.app_context(): + result = annif.rest.reconcile_suggest("nonexistent", prefix="example text") + assert result.status_code == 404 From d1f90dfd7707848161ed6785a03fc6b44f1d963c Mon Sep 17 00:00:00 2001 From: Unni Kohonen Date: Wed, 29 Nov 2023 17:13:59 +0200 Subject: [PATCH 7/7] Fix suggest service url --- annif/rest.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/annif/rest.py b/annif/rest.py index bc57689ee..27ee3c704 100644 --- a/annif/rest.py +++ b/annif/rest.py @@ -259,9 +259,7 @@ def reconcile_metadata( "suggest": { "entity": { "service_path": "/suggest/entity", - "service_url": "http://localhost:5000/v1/projects/" - + project_id - + "/reconcile", # change to actual host url (how?) + "service_url": connexion.request.base_url } }, }