diff --git a/scripts/constant.py b/scripts/constant.py index c719768..1eb3992 100644 --- a/scripts/constant.py +++ b/scripts/constant.py @@ -96,6 +96,7 @@ DELETE_ROOT_NETWORKS = STUDY_SERVER_URL + "/supervision/root-networks" GET_DIRECTORY_ELEMENTS = DIRECTORY_SERVER_URL + "/supervision/elements" +GET_UNMODIFIED_DIRECTORY_ELEMENTS = DIRECTORY_SERVER_URL + "/supervision/elements/unmodified" GET_SUPERVISION_STUDIES = STUDY_SERVER_URL + "/supervision/studies" GET_DYNAMIC_MAPPING_FILTERS = DYNAMIC_MAPPING_SERVER_URL + "/supervision/filters" @@ -122,6 +123,7 @@ RECREATE_STUDY_INDICES = STUDY_SERVER_URL + "/supervision/studies/indices" DELETE_STUDY_INDEXED_EQUIPMENTS_BY_NETWORK_UUID = STUDY_SERVER_URL + "/supervision/studies/{networkUuid}/indexed-equipments-by-network-uuid" DELETE_STUDY_NODES_BUILDS = STUDY_SERVER_URL + "/supervision/studies/{studyUuid}/nodes/builds" +INVALIDATE_STUDY = STUDY_SERVER_URL + "/supervision/studies/{studyUuid}/invalidate" GET_STUDIES_INDEXED_STUDIES_COUNT = STUDY_SERVER_URL + "/supervision/studies/indexation-count" GET_STUDIES_INDEXED_EQUIPMENTS_COUNT = STUDY_SERVER_URL + "/supervision/equipments/indexation-count" GET_STUDIES_INDEXED_TOMBSTONED_EQUIPMENTS_COUNT = STUDY_SERVER_URL + "/supervision/tombstoned-equipments/indexation-count" diff --git a/scripts/functions/studies/studies.py b/scripts/functions/studies/studies.py index 503cfe1..624bc7f 100644 --- a/scripts/functions/studies/studies.py +++ b/scripts/functions/studies/studies.py @@ -36,4 +36,7 @@ def delete_indexed_equipments(networkUuid): def invalidate_nodes_builds(study_uuid): return requests.delete(constant.DELETE_STUDY_NODES_BUILDS.format(studyUuid = study_uuid)) + +def invalidate_study(study_uuid): + return requests.delete(constant.INVALIDATE_STUDY.format(studyUuid = study_uuid)) diff --git a/scripts/invalidate_unmodified_studies.py b/scripts/invalidate_unmodified_studies.py new file mode 100644 index 0000000..0593c91 --- /dev/null +++ b/scripts/invalidate_unmodified_studies.py @@ -0,0 +1,105 @@ +# +# Copyright (c) 2026, RTE (http://www.rte-france.com) +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# + +import sys +import requests +import constant +from functions.studies.studies import invalidate_study +from tqdm import tqdm + +# +# Invalidates built nodes and delete initial variant network for all studies that have not been modified since a given duration. +# +# Usage: +# python invalidate_unmodified_studies.py [--dry-run] [--limit ] +# +# Arguments: +# duration ISO 8601 duration (e.g. P365D for 1 year, P30D for 30 days, PT24H for 24 hours) +# --dry-run Optional flag to only list affected studies without performing any invalidation +# --limit Optional maximum number of studies to process +# +# Example: +# python invalidate_unmodified_studies.py P365D --dry-run +# python invalidate_unmodified_studies.py P365D --limit 10 --dry-run +# + +# +# @author Hugo Marcellin +# + +def get_unmodified_studies(duration): + response = requests.get(constant.GET_UNMODIFIED_DIRECTORY_ELEMENTS, params={"elementType": "STUDY", "duration": duration}) + response.raise_for_status() + return response.json() + +def invalidate_unmodified_studies(duration, dry_run=False, limit=None): + if constant.DEV: + print(f"\nDEV={str(constant.DEV)} -> hostnames configured for a local execution (172.17.0.1:xxxx)") + + print(f"Fetching studies not modified since {duration}...") + studies = get_unmodified_studies(duration) + + if not studies: + print("No unmodified studies found.") + return + + print(f"Found {len(studies)} unmodified study/studies.") + + if limit is not None and limit < len(studies): + print(f"Limit applied: processing {limit} out of {len(studies)} studies.") + studies = studies[:limit] + + print("Selected studies:") + for study in studies: + print(f" - {study['elementUuid']} | {study['elementName']} | last modified: {study['lastModificationDate']}") + + if dry_run: + print("\nDry run mode: no study will be invalidated.") + return + + print("\nUnmounting studies...") + success_count = 0 + failure_count = 0 + for study in tqdm(studies): + try: + study_uuid = study["elementUuid"] + result = invalidate_study(study_uuid) + result.raise_for_status() + success_count += 1 + except Exception as e: + failure_count += 1 + tqdm.write(f" FAILED - {study_uuid} (error: {str(e)})") + if isinstance(e, requests.exceptions.RequestException) and e.response is not None: + tqdm.write("Response body: " + repr(e.response.text)) # repr for cheap escaping + tqdm.write("") # emtpy newline between errors for legibility + + print(f"\nDone. {success_count} succeeded, {failure_count} failed.") + + +if len(sys.argv) < 2: + print("Usage: python invalidate_unmodified_studies.py [--dry-run] [--limit ]") + print("Example: python invalidate_unmodified_studies.py P365D --limit 10 --dry-run") + sys.exit(1) + +duration_arg = sys.argv[1] +dry_run_arg = "--dry-run" in sys.argv + +limit_arg = None +if "--limit" in sys.argv: + limit_index = sys.argv.index("--limit") + if limit_index + 1 >= len(sys.argv): + print("Error: --limit requires a numeric value.") + sys.exit(1) + try: + limit_arg = int(sys.argv[limit_index + 1]) + if limit_arg <= 0: + raise ValueError + except ValueError: + print("Error: --limit must be a positive integer.") + sys.exit(1) + +invalidate_unmodified_studies(duration_arg, dry_run=dry_run_arg, limit=limit_arg)