From 5f6b25649c6a79eb80050c7e18a9749b7e77dc73 Mon Sep 17 00:00:00 2001 From: Mathias Brunkow Moser Date: Wed, 15 Nov 2023 11:04:00 +0100 Subject: [PATCH 1/8] fix: added check for empty or null contractIds with retrial --- .../http/controllers/api/ContractController.java | 2 +- .../tractusx/productpass/managers/DtrSearchManager.java | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/consumer-backend/productpass/src/main/java/org/eclipse/tractusx/productpass/http/controllers/api/ContractController.java b/consumer-backend/productpass/src/main/java/org/eclipse/tractusx/productpass/http/controllers/api/ContractController.java index a553567f5..f5c095fed 100644 --- a/consumer-backend/productpass/src/main/java/org/eclipse/tractusx/productpass/http/controllers/api/ContractController.java +++ b/consumer-backend/productpass/src/main/java/org/eclipse/tractusx/productpass/http/controllers/api/ContractController.java @@ -187,7 +187,7 @@ public Response create(@Valid @RequestBody DiscoverySearch searchBody) { for(Dtr dtr: dtrs){ Long validUntil = dtr.getValidUntil(); - if(validUntil == null || validUntil < currentTimestamp){ + if(dtr.getContractId() == null || dtr.getContractId().isEmpty() || validUntil == null || validUntil < currentTimestamp){ requestDtrs = true; // If the cache invalidation time has come request Dtrs break; } diff --git a/consumer-backend/productpass/src/main/java/org/eclipse/tractusx/productpass/managers/DtrSearchManager.java b/consumer-backend/productpass/src/main/java/org/eclipse/tractusx/productpass/managers/DtrSearchManager.java index 63f5154a9..187f86df9 100644 --- a/consumer-backend/productpass/src/main/java/org/eclipse/tractusx/productpass/managers/DtrSearchManager.java +++ b/consumer-backend/productpass/src/main/java/org/eclipse/tractusx/productpass/managers/DtrSearchManager.java @@ -426,7 +426,7 @@ private Runnable createAndSaveDtr(Dataset dataset, String bpn, String connection public void run() { try { Offer offer = dataTransferService.buildOffer(dataset, 0); - String builtDataEndpoint =CatenaXUtil.buildDataEndpoint(connectionUrl); + String builtDataEndpoint = CatenaXUtil.buildDataEndpoint(connectionUrl); IdResponse negotiationResponse = dataTransferService.doContractNegotiation(offer, bpn, builtDataEndpoint); if (negotiationResponse == null) { return; @@ -436,6 +436,10 @@ public void run() { LogUtil.printWarning("It was not possible to do ContractNegotiation for URL: " + connectionUrl); return; } + if(negotiation.getContractAgreementId() == null || negotiation.getContractAgreementId().isEmpty()){ + LogUtil.printError("It was not possible to get an Contract Agreemment Id for the URL: " + connectionUrl); + return; + } Dtr dtr = new Dtr(negotiation.getContractAgreementId(), connectionUrl, offer.getAssetId(), bpn, DateTimeUtil.addHoursToCurrentTimestamp(dtrConfig.getTemporaryStorage().getLifetime())); if (dtrConfig.getTemporaryStorage().getEnabled()) { addConnectionToBpnEntry(bpn, dtr); From 7eaa2476f805e9de5567700b32c36eedbe76f575 Mon Sep 17 00:00:00 2001 From: Mathias Brunkow Moser Date: Wed, 15 Nov 2023 11:47:41 +0100 Subject: [PATCH 2/8] feat: added timeout in dtr negotiation process --- charts/digital-product-pass/values-beta.yaml | 3 ++- charts/digital-product-pass/values-dev.yaml | 3 ++- charts/digital-product-pass/values-int.yaml | 3 ++- charts/digital-product-pass/values.yaml | 3 ++- .../tractusx/productpass/config/DtrConfig.java | 9 +++++++++ .../productpass/managers/DtrSearchManager.java | 13 +++++++------ .../productpass/src/main/java/utils/ThreadUtil.java | 4 ++-- .../productpass/src/main/resources/application.yml | 3 ++- 8 files changed, 28 insertions(+), 13 deletions(-) diff --git a/charts/digital-product-pass/values-beta.yaml b/charts/digital-product-pass/values-beta.yaml index 9d8affa6f..311b77dbb 100644 --- a/charts/digital-product-pass/values-beta.yaml +++ b/charts/digital-product-pass/values-beta.yaml @@ -148,7 +148,8 @@ backend: subModel: "/submodel-descriptors" timeouts: search: 10 - negotiation: 40 + negotiation: 15 + dtrRequestProcess: 40 transfer: 10 digitalTwin: 20 temporaryStorage: true diff --git a/charts/digital-product-pass/values-dev.yaml b/charts/digital-product-pass/values-dev.yaml index 3a1f8e186..496114398 100644 --- a/charts/digital-product-pass/values-dev.yaml +++ b/charts/digital-product-pass/values-dev.yaml @@ -148,7 +148,8 @@ backend: subModel: "/submodel-descriptors" timeouts: search: 10 - negotiation: 40 + negotiation: 15 + dtrRequestProcess: 40 transfer: 10 digitalTwin: 20 temporaryStorage: true diff --git a/charts/digital-product-pass/values-int.yaml b/charts/digital-product-pass/values-int.yaml index 3ecb014ad..fe1c4f8f7 100644 --- a/charts/digital-product-pass/values-int.yaml +++ b/charts/digital-product-pass/values-int.yaml @@ -147,7 +147,8 @@ backend: subModel: "/submodel-descriptors" timeouts: search: 10 - negotiation: 40 + negotiation: 15 + dtrRequestProcess: 40 transfer: 10 digitalTwin: 20 temporaryStorage: true diff --git a/charts/digital-product-pass/values.yaml b/charts/digital-product-pass/values.yaml index 274a8a32c..299507f09 100644 --- a/charts/digital-product-pass/values.yaml +++ b/charts/digital-product-pass/values.yaml @@ -177,7 +177,8 @@ backend: # -- timeouts for the digital twin registry async negotiation timeouts: search: 10 - negotiation: 40 + negotiation: 15 + dtrRequestProcess: 40 transfer: 10 digitalTwin: 20 # -- temporary storage of dDTRs for optimization diff --git a/consumer-backend/productpass/src/main/java/org/eclipse/tractusx/productpass/config/DtrConfig.java b/consumer-backend/productpass/src/main/java/org/eclipse/tractusx/productpass/config/DtrConfig.java index a23885c93..26ab220cb 100644 --- a/consumer-backend/productpass/src/main/java/org/eclipse/tractusx/productpass/config/DtrConfig.java +++ b/consumer-backend/productpass/src/main/java/org/eclipse/tractusx/productpass/config/DtrConfig.java @@ -152,6 +152,7 @@ public static class Timeouts{ Integer negotiation; Integer transfer; Integer digitalTwin; + Integer dtrRequestProcess; /** GETTERS AND SETTERS **/ public Integer getSearch() { @@ -179,6 +180,14 @@ public Integer getDigitalTwin() { public void setDigitalTwin(Integer digitalTwin) { this.digitalTwin = digitalTwin; } + + public Integer getDtrRequestProcess() { + return dtrRequestProcess; + } + + public void setDtrRequestProcess(Integer dtrRequestProcess) { + this.dtrRequestProcess = dtrRequestProcess; + } } /** diff --git a/consumer-backend/productpass/src/main/java/org/eclipse/tractusx/productpass/managers/DtrSearchManager.java b/consumer-backend/productpass/src/main/java/org/eclipse/tractusx/productpass/managers/DtrSearchManager.java index 187f86df9..554f336e6 100644 --- a/consumer-backend/productpass/src/main/java/org/eclipse/tractusx/productpass/managers/DtrSearchManager.java +++ b/consumer-backend/productpass/src/main/java/org/eclipse/tractusx/productpass/managers/DtrSearchManager.java @@ -69,7 +69,7 @@ public class DtrSearchManager { private ConcurrentHashMap> dtrDataModel; private ConcurrentHashMap catalogsCache; private final long searchTimeoutSeconds; - private final long negotiationTimeoutSeconds; + private final long dtrRequestProcessTimeout; private final String fileName = "dtrDataModel.json"; private String dtrDataModelFilePath; private State state; @@ -93,8 +93,7 @@ public DtrSearchManager(FileUtil fileUtil, JsonUtil jsonUtil, DataTransferServic this.dtrDataModelFilePath = this.createDataModelFile(); this.dtrDataModel = this.loadDtrDataModel(); this.searchTimeoutSeconds = this.dtrConfig.getTimeouts().getSearch(); - this.negotiationTimeoutSeconds = this.dtrConfig.getTimeouts().getNegotiation(); - + this.dtrRequestProcessTimeout = this.dtrConfig.getTimeouts().getDtrRequestProcess(); } /** GETTERS AND SETTERS **/ @@ -230,7 +229,7 @@ public void searchEndpoint(String processId, String bpn, String endpoint){ if (dataset != null) { Thread singleOfferThread = ThreadUtil.runThread(createAndSaveDtr(dataset, bpn, endpoint, processId), "CreateAndSaveDtr"); try { - if (!singleOfferThread.join(Duration.ofSeconds(negotiationTimeoutSeconds))) { + if (!singleOfferThread.join(Duration.ofSeconds(this.dtrRequestProcessTimeout))) { singleOfferThread.interrupt(); LogUtil.printWarning("Failed to retrieve the Catalog due a timeout for the URL: " + endpoint); return; @@ -248,7 +247,7 @@ public void searchEndpoint(String processId, String bpn, String endpoint){ contractOfferList.parallelStream().forEach(dataset -> { Thread multipleOffersThread = ThreadUtil.runThread(createAndSaveDtr(dataset, bpn, endpoint, processId), "CreateAndSaveDtr"); try { - if (!multipleOffersThread.join(Duration.ofSeconds(negotiationTimeoutSeconds))) { + if (!multipleOffersThread.join(Duration.ofSeconds(this.dtrRequestProcessTimeout))) { multipleOffersThread.interrupt(); LogUtil.printWarning("Failed to retrieve the Catalog due a timeout for the URL: " + endpoint); } @@ -431,7 +430,9 @@ public void run() { if (negotiationResponse == null) { return; } - Negotiation negotiation = dataTransferService.seeNegotiation(negotiationResponse.getId()); + Integer millis = dtrConfig.getTimeouts().getNegotiation() * 1000; // Set max timeout from seconds to milliseconds + // If negotiation takes way too much time give timeout + Negotiation negotiation = ThreadUtil.timeout(millis, ()->dataTransferService.seeNegotiation(negotiationResponse.getId()), null); if (negotiation == null) { LogUtil.printWarning("It was not possible to do ContractNegotiation for URL: " + connectionUrl); return; diff --git a/consumer-backend/productpass/src/main/java/utils/ThreadUtil.java b/consumer-backend/productpass/src/main/java/utils/ThreadUtil.java index d92333315..da95667a0 100644 --- a/consumer-backend/productpass/src/main/java/utils/ThreadUtil.java +++ b/consumer-backend/productpass/src/main/java/utils/ThreadUtil.java @@ -134,12 +134,12 @@ public static V timeout(Integer milliseconds, Callable function, V timeou { try { ExecutorService executor = Executors.newSingleThreadExecutor(); - Future future = executor.submit(function); boolean timeout = false; V returnObject = null; try { + Future future = executor.submit(function); returnObject = future.get(milliseconds, TimeUnit.MILLISECONDS); - } catch (TimeoutException e) { + } catch (Exception e) { timeout = true; } executor.shutdownNow(); diff --git a/consumer-backend/productpass/src/main/resources/application.yml b/consumer-backend/productpass/src/main/resources/application.yml index 778385128..89eb9a3be 100644 --- a/consumer-backend/productpass/src/main/resources/application.yml +++ b/consumer-backend/productpass/src/main/resources/application.yml @@ -83,7 +83,8 @@ configuration: subModel: "/submodel-descriptors" timeouts: search: 10 - negotiation: 40 + negotiation: 15 + dtrRequestProcess: 40 transfer: 10 digitalTwin: 20 temporaryStorage: From 4f3404ebb4853507d9d3f0fb6d6b3c93ddcc55bd Mon Sep 17 00:00:00 2001 From: Mathias Brunkow Moser Date: Wed, 15 Nov 2023 13:10:16 +0100 Subject: [PATCH 3/8] fix: fixed the timeout time for each negotiation --- .../productpass/managers/DtrSearchManager.java | 10 +++++----- .../productpass/src/main/resources/application.yml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/consumer-backend/productpass/src/main/java/org/eclipse/tractusx/productpass/managers/DtrSearchManager.java b/consumer-backend/productpass/src/main/java/org/eclipse/tractusx/productpass/managers/DtrSearchManager.java index 554f336e6..999163b44 100644 --- a/consumer-backend/productpass/src/main/java/org/eclipse/tractusx/productpass/managers/DtrSearchManager.java +++ b/consumer-backend/productpass/src/main/java/org/eclipse/tractusx/productpass/managers/DtrSearchManager.java @@ -204,7 +204,7 @@ public void run() { } public void searchEndpoint(String processId, String bpn, String endpoint){ //Search Digital Twin Catalog for each connectionURL with a timeout time - Thread asyncThread = ThreadUtil.runThread(searchDigitalTwinCatalogExecutor(endpoint), "ProcessDtrDataModel"); + Thread asyncThread = ThreadUtil.runThread(searchDigitalTwinCatalogExecutor(endpoint), "SearchEndpoint"+processId+"-"+bpn+"-"+endpoint); try { if (!asyncThread.join(Duration.ofSeconds(searchTimeoutSeconds))) { asyncThread.interrupt(); @@ -227,11 +227,11 @@ public void searchEndpoint(String processId, String bpn, String endpoint){ if (contractOffers instanceof LinkedHashMap) { Dataset dataset = (Dataset) jsonUtil.bindObject(contractOffers, Dataset.class); if (dataset != null) { - Thread singleOfferThread = ThreadUtil.runThread(createAndSaveDtr(dataset, bpn, endpoint, processId), "CreateAndSaveDtr"); + Thread singleOfferThread = ThreadUtil.runThread(createAndSaveDtr(dataset, bpn, endpoint, processId), "CreateAndSaveDtr-"+processId+"-"+bpn+"-"+endpoint); try { if (!singleOfferThread.join(Duration.ofSeconds(this.dtrRequestProcessTimeout))) { singleOfferThread.interrupt(); - LogUtil.printWarning("Failed to retrieve the Catalog due a timeout for the URL: " + endpoint); + LogUtil.printWarning("Failed to retrieve do contract negotiations due a timeout for the URL: " + endpoint); return; } } catch (InterruptedException e) { @@ -245,11 +245,11 @@ public void searchEndpoint(String processId, String bpn, String endpoint){ return; } contractOfferList.parallelStream().forEach(dataset -> { - Thread multipleOffersThread = ThreadUtil.runThread(createAndSaveDtr(dataset, bpn, endpoint, processId), "CreateAndSaveDtr"); + Thread multipleOffersThread = ThreadUtil.runThread(createAndSaveDtr(dataset, bpn, endpoint, processId), "CreateAndSaveDtr-"+processId+"-"+bpn+"-"+endpoint); try { if (!multipleOffersThread.join(Duration.ofSeconds(this.dtrRequestProcessTimeout))) { multipleOffersThread.interrupt(); - LogUtil.printWarning("Failed to retrieve the Catalog due a timeout for the URL: " + endpoint); + LogUtil.printWarning("Failed to retrieve the contract negotiations due a timeout for the URL: " + endpoint); } } catch (InterruptedException e) { throw new RuntimeException(e); diff --git a/consumer-backend/productpass/src/main/resources/application.yml b/consumer-backend/productpass/src/main/resources/application.yml index 89eb9a3be..35bb2e66a 100644 --- a/consumer-backend/productpass/src/main/resources/application.yml +++ b/consumer-backend/productpass/src/main/resources/application.yml @@ -83,7 +83,7 @@ configuration: subModel: "/submodel-descriptors" timeouts: search: 10 - negotiation: 15 + negotiation: 10 dtrRequestProcess: 40 transfer: 10 digitalTwin: 20 From 3bea5f86587d964e94d46d3c1320894748071575 Mon Sep 17 00:00:00 2001 From: Mathias Brunkow Moser Date: Wed, 15 Nov 2023 14:02:28 +0100 Subject: [PATCH 4/8] fix: added descriptive logs to search and create methods --- .../http/controllers/api/ContractController.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/consumer-backend/productpass/src/main/java/org/eclipse/tractusx/productpass/http/controllers/api/ContractController.java b/consumer-backend/productpass/src/main/java/org/eclipse/tractusx/productpass/http/controllers/api/ContractController.java index f5c095fed..ad66e8036 100644 --- a/consumer-backend/productpass/src/main/java/org/eclipse/tractusx/productpass/http/controllers/api/ContractController.java +++ b/consumer-backend/productpass/src/main/java/org/eclipse/tractusx/productpass/http/controllers/api/ContractController.java @@ -155,6 +155,7 @@ public Response create(@Valid @RequestBody DiscoverySearch searchBody) { return httpUtil.buildResponse(response, httpResponse); } String processId = processManager.initProcess(); + LogUtil.printMessage("Creating process [" + processId + "] for "+searchBody.getType() + ": "+ searchBody.getId()); ConcurrentHashMap> dataModel = null; if(dtrConfig.getTemporaryStorage().getEnabled()) { try { @@ -270,7 +271,7 @@ public Response search(@Valid @RequestBody Search searchBody) { response = httpUtil.getBadRequest("No processId was found on the request body!"); return httpUtil.buildResponse(response, httpResponse); } - + String processId = searchBody.getProcessId(); if(processId.isEmpty()){ response = httpUtil.getBadRequest("Process id is required for decentral digital twin registry searches!"); @@ -286,9 +287,12 @@ public Response search(@Valid @RequestBody Search searchBody) { return httpUtil.buildResponse(response, httpResponse); } Boolean childrenCondition = searchBody.getChildren(); + String logPrint = "[" + processId + "] Creating search for "+searchBody.getIdType() + ": "+ searchBody.getId(); if(childrenCondition != null){ + LogUtil.printMessage(logPrint + " with drilldown enabled"); process = processManager.createProcess(processId, childrenCondition, httpRequest); // Store the children condition }else { + LogUtil.printMessage(logPrint + " with drilldown disabled"); process = processManager.createProcess(processId, httpRequest); } Status status = processManager.getStatus(processId); From b4c40357f5e2e88c3e2265fce44c700f75e6bcda Mon Sep 17 00:00:00 2001 From: Mathias Brunkow Moser Date: Thu, 16 Nov 2023 10:54:32 +0100 Subject: [PATCH 5/8] fix: fixed edc configuration --- .../templates/deployment-backend.yaml | 4 ++-- charts/digital-product-pass/values-beta.yaml | 11 +++++------ charts/digital-product-pass/values-dev.yaml | 11 +++++------ charts/digital-product-pass/values-int.yaml | 11 +++++------ 4 files changed, 17 insertions(+), 20 deletions(-) diff --git a/charts/digital-product-pass/templates/deployment-backend.yaml b/charts/digital-product-pass/templates/deployment-backend.yaml index fe1208972..8be65d12f 100644 --- a/charts/digital-product-pass/templates/deployment-backend.yaml +++ b/charts/digital-product-pass/templates/deployment-backend.yaml @@ -79,8 +79,8 @@ spec: - name: backend-config mountPath: /app/config - name: pvc-backend - mountPath: /app/data - subPath: data + mountPath: /app/data/process + subPath: data/process - name: pvc-backend mountPath: /app/log subPath: log diff --git a/charts/digital-product-pass/values-beta.yaml b/charts/digital-product-pass/values-beta.yaml index 311b77dbb..cbfc1bdcb 100644 --- a/charts/digital-product-pass/values-beta.yaml +++ b/charts/digital-product-pass/values-beta.yaml @@ -78,12 +78,11 @@ backend: hosts: - materialpass.beta.demo.catena-x.net - avp: - helm: - clientId: - clientSecret: - xApiKey: - participantId: + edc: + clientId: + clientSecret: + xApiKey: + participantId: - clientSecret: - xApiKey: - participantId: + edc: + clientId: + clientSecret: + xApiKey: + participantId: - clientSecret: - xApiKey: - participantId: + edc: + clientId: + clientSecret: + xApiKey: + participantId: application: yml: |- From 30e97b8e39a370dec2eb79f9f71d6d86776c5e2d Mon Sep 17 00:00:00 2001 From: Muhammad Saud Khan Date: Wed, 13 Dec 2023 17:36:19 +0100 Subject: [PATCH 6/8] fix: fixed typo in values files --- charts/digital-product-pass/values-beta.yaml | 8 ++++---- charts/digital-product-pass/values-dev.yaml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/charts/digital-product-pass/values-beta.yaml b/charts/digital-product-pass/values-beta.yaml index cbfc1bdcb..fbb89eec1 100644 --- a/charts/digital-product-pass/values-beta.yaml +++ b/charts/digital-product-pass/values-beta.yaml @@ -79,10 +79,10 @@ backend: - materialpass.beta.demo.catena-x.net edc: - clientId: - clientSecret: - xApiKey: - participantId: + clientSecret: + xApiKey: + participantId: application: yml: |- diff --git a/charts/digital-product-pass/values-dev.yaml b/charts/digital-product-pass/values-dev.yaml index e88d6248c..68eebe67c 100644 --- a/charts/digital-product-pass/values-dev.yaml +++ b/charts/digital-product-pass/values-dev.yaml @@ -82,7 +82,7 @@ backend: clientId: clientSecret: xApiKey: - participantId: application: yml: |- From bc4771a5e64728b537bb914110193ce935e65b7e Mon Sep 17 00:00:00 2001 From: Muhammad Saud Khan Date: Wed, 13 Dec 2023 18:12:14 +0100 Subject: [PATCH 7/8] chore: added changelogs and user readme --- CHANGELOG.md | 22 ++++++++++++++++++++++ docs/RELEASE_USER.md | 10 ++++++++++ 2 files changed, 32 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82018a3b3..bfa0a3809 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,28 @@ The changelog format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [unreleased] +## [v1.4.0] - 14-12-2023 + +## Added +- Added script to automate the uploading of various passport types +- Added script to delete data from the data provider +- Added check for empty or null contractIds with retry attempts +- Added descriptive logs to search and create methods + +## Updated +- Updated ingress settings and backend configuration in the helm chart +- Refactored helm values to show only user relevant settings + +## Issued Fixed +- Fixed the timeout time for each negotiation +- Fixed the long waiting time by implementing timeout when doing the negotiation +- Fixed the null contract ids creation + +## Deleted +- Remove the legacy style to register/delete the testdata from the data provider + + ## [released] ## [v1.3.1] - 08-11-2023 diff --git a/docs/RELEASE_USER.md b/docs/RELEASE_USER.md index c350cdba9..b867ce359 100644 --- a/docs/RELEASE_USER.md +++ b/docs/RELEASE_USER.md @@ -23,6 +23,16 @@ # Release Notes Digital Product Pass Application User friendly relase notes without especific technical details. +**November 14 2023 (Version 1.4.0)** +*14.12.2023* + +### Added +#### DPP test data uploader +A script is refactored to upload/remove testdata set from the data provider setup. This speeds up the automatic uploading of various passes types into the provider's digital twin registry, data service and EDC connector. + +### Updated +#### Optimize contract negotiation time +There was a long waiting time during the contract negotiation. This time is now reduced and the negotiation is perfomred faster. **November 08 2023 (Version 1.3.1)** *08.11.2023* From af532e613ff48ff25ee3b70750793ac7f262baad Mon Sep 17 00:00:00 2001 From: Muhammad Saud Khan Date: Thu, 14 Dec 2023 16:44:09 +0100 Subject: [PATCH 8/8] chore: update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bfa0a3809..9cc638b06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ The changelog format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [unreleased] +## [released] ## [v1.4.0] - 14-12-2023 ## Added