From 30dc41f6f7c999abfd6044bd5755470e1a395739 Mon Sep 17 00:00:00 2001 From: Sam Novod Date: Wed, 27 Aug 2025 15:56:54 -0400 Subject: [PATCH 1/9] Adding methods to check if file/dir is writtable --- VERSION.txt | 4 +- ops_utils/gcp_utils.py | 140 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 141 insertions(+), 3 deletions(-) diff --git a/VERSION.txt b/VERSION.txt index 2d201c4..2cb30a2 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1,2 +1,2 @@ -11.3.2 -- Adding tests +11.4.0 +- Adding methods to check if gcp path is writeable diff --git a/ops_utils/gcp_utils.py b/ops_utils/gcp_utils.py index 5577a1e..8635ae2 100644 --- a/ops_utils/gcp_utils.py +++ b/ops_utils/gcp_utils.py @@ -11,6 +11,7 @@ from mimetypes import guess_type from typing import Optional, Any from google.cloud.storage.blob import Blob +from google.api_core.exceptions import Forbidden, GoogleAPICallError from google.oauth2 import service_account from google.cloud import storage from google.auth import default @@ -729,4 +730,141 @@ def get_active_gcloud_account() -> str: ) return result.stdout.strip() - + def has_write_permission(self, cloud_path: str) -> bool: + """ + Check if the current user has permission to write to a GCP path. + + This method tests write access by attempting to update the metadata + of an existing blob or create a zero-byte temporary file if the blob + doesn't exist. The temporary file is deleted immediately if created. + + **Args:** + - cloud_path (str): The GCS path to check for write permissions. + + **Returns:** + - bool: True if the user has write permission, False otherwise. + """ + try: + if cloud_path.endswith("/"): + logging.warning(f"Provided cloud path {cloud_path} is a directory, will check {cloud_path}/permission_test_temp") + cloud_path = f"{cloud_path}permission_test_temp" + + # Process the cloud path to get the bucket and blob name + components = self._process_cloud_path(cloud_path) + bucket_name = components["bucket"] + + # Get the bucket with user_project set for requester pays buckets + bucket = self.client.bucket(bucket_name, user_project=self.client.project) + + # Check if we can access the bucket at all + if not bucket.exists(): + logging.warning(f"Bucket {bucket_name} does not exist or you don't have access to it") + return False + + # Use the existing load_blob_from_full_path method to get the blob + blob = self.load_blob_from_full_path(cloud_path) + blob_exists = blob.exists() + + if blob_exists: + # Try updating metadata (doesn't change the content) + try: + original_metadata = blob.metadata or {} + test_metadata = original_metadata.copy() + test_metadata["_write_permission_test"] = "true" + + blob.metadata = test_metadata + blob.patch() + + # Restore the original metadata + blob.metadata = original_metadata + blob.patch() + + logging.info(f"Write permission confirmed for existing blob {cloud_path}") + return True + except Forbidden: + logging.error(f"No write permission on existing blob {cloud_path}") + return False + except GoogleAPICallError as e: + logging.error(f"Error accessing blob {cloud_path}: {e}") + return False + else: + # If blob doesn't exist, try to create a temporary zero-byte file + # in the same directory to test write permissions + test_file = f"{cloud_path}.permission_test_temp" + test_blob = self.load_blob_from_full_path(test_file) + + try: + # Try writing a temporary file to the bucket + test_blob.upload_from_string("") + # Clean up the test file + test_blob.delete() + logging.info(f"Write permission confirmed for new path {cloud_path}") + return True + except Forbidden: + logging.error(f"No write permission on path {cloud_path}") + return False + except GoogleAPICallError as e: + logging.error(f"Error accessing path {cloud_path}: {e}") + return False + + except Exception as e: + logging.warning(f"Failed to check write permission for {cloud_path}: {str(e)}") + return False + + def wait_for_write_permission(self, cloud_path: str, interval_wait_time_minutes: int = 1, max_wait_time_minutes: int = 30) -> bool: + """ + Wait for write permissions on a GCP path, checking at regular intervals. + + This method will periodically check if the user has write permission on the specified cloud path. + It will continue checking until either write permission is granted or the maximum wait time is reached. + + **Args:** + - cloud_path (str): The GCS path to check for write permissions. + - interval_wait_time_minutes (int, optional): Time in minutes to wait between permission checks. Defaults to 1 minute. + - max_wait_time_minutes (int, optional): Maximum time in minutes to wait for permissions. Defaults to 30 minutes. + + **Returns:** + - bool: True if write permission is granted within the wait time, False otherwise. + """ + import time + + # Convert minutes to seconds for the sleep function + interval_seconds = interval_wait_time_minutes * 60 + max_wait_seconds = max_wait_time_minutes * 60 + + start_time = time.time() + attempt_number = 1 + + logging.info(f"Starting to check for write permissions on {cloud_path}") + logging.info(f"Will check every {interval_wait_time_minutes} minute(s) for up to {max_wait_time_minutes} minute(s)") + + # First check immediately + if self.has_write_permission(cloud_path): + logging.info(f"Write permission confirmed on initial check for {cloud_path}") + return True + + # If first check fails, start periodic checks + while time.time() - start_time < max_wait_seconds: + elapsed_minutes = (time.time() - start_time) / 60 + remaining_minutes = max_wait_time_minutes - elapsed_minutes + + logging.info(f"Waiting {interval_wait_time_minutes} minute(s) before next permission check. " + f"Time elapsed: {elapsed_minutes:.1f} minute(s). " + f"Time remaining: {remaining_minutes:.1f} minute(s).") + + # Sleep for the interval duration + time.sleep(interval_seconds) + + attempt_number += 1 + logging.info(f"Checking write permissions (attempt {attempt_number})...") + + if self.has_write_permission(cloud_path): + elapsed_minutes = (time.time() - start_time) / 60 + logging.info(f"Write permission confirmed after {elapsed_minutes:.1f} minute(s) on attempt {attempt_number}") + return True + + # If we get here, we've exceeded the maximum wait time + logging.warning(f"Maximum wait time of {max_wait_time_minutes} minute(s) exceeded. " + f"Write permission was not granted for {cloud_path} after {attempt_number} attempts.") + return False + From a013804c24bfe3d7f2ed4faa0074cab7016b153b Mon Sep 17 00:00:00 2001 From: Sam Novod Date: Wed, 27 Aug 2025 16:08:50 -0400 Subject: [PATCH 2/9] ... --- ops_utils/gcp_utils.py | 127 ++++++++++++++++++++--------------------- 1 file changed, 62 insertions(+), 65 deletions(-) diff --git a/ops_utils/gcp_utils.py b/ops_utils/gcp_utils.py index 8635ae2..fa76a0e 100644 --- a/ops_utils/gcp_utils.py +++ b/ops_utils/gcp_utils.py @@ -1,6 +1,7 @@ """Module for GCP utilities.""" import os import logging +import time import io import json import hashlib @@ -744,74 +745,69 @@ def has_write_permission(self, cloud_path: str) -> bool: **Returns:** - bool: True if the user has write permission, False otherwise. """ - try: - if cloud_path.endswith("/"): - logging.warning(f"Provided cloud path {cloud_path} is a directory, will check {cloud_path}/permission_test_temp") - cloud_path = f"{cloud_path}permission_test_temp" + if cloud_path.endswith("/"): + logging.warning(f"Provided cloud path {cloud_path} is a directory, will check {cloud_path}/permission_test_temp") + cloud_path = f"{cloud_path}permission_test_temp" - # Process the cloud path to get the bucket and blob name - components = self._process_cloud_path(cloud_path) - bucket_name = components["bucket"] + # Process the cloud path to get the bucket and blob name + components = self._process_cloud_path(cloud_path) + bucket_name = components["bucket"] - # Get the bucket with user_project set for requester pays buckets - bucket = self.client.bucket(bucket_name, user_project=self.client.project) + # Get the bucket with user_project set for requester pays buckets + bucket = self.client.bucket(bucket_name, user_project=self.client.project) - # Check if we can access the bucket at all - if not bucket.exists(): - logging.warning(f"Bucket {bucket_name} does not exist or you don't have access to it") - return False + # Check if we can access the bucket at all + if not bucket.exists(): + logging.warning(f"Bucket {bucket_name} does not exist or you don't have access to it") + return False - # Use the existing load_blob_from_full_path method to get the blob - blob = self.load_blob_from_full_path(cloud_path) - blob_exists = blob.exists() - - if blob_exists: - # Try updating metadata (doesn't change the content) - try: - original_metadata = blob.metadata or {} - test_metadata = original_metadata.copy() - test_metadata["_write_permission_test"] = "true" - - blob.metadata = test_metadata - blob.patch() - - # Restore the original metadata - blob.metadata = original_metadata - blob.patch() - - logging.info(f"Write permission confirmed for existing blob {cloud_path}") - return True - except Forbidden: - logging.error(f"No write permission on existing blob {cloud_path}") - return False - except GoogleAPICallError as e: - logging.error(f"Error accessing blob {cloud_path}: {e}") - return False - else: - # If blob doesn't exist, try to create a temporary zero-byte file - # in the same directory to test write permissions - test_file = f"{cloud_path}.permission_test_temp" - test_blob = self.load_blob_from_full_path(test_file) - - try: - # Try writing a temporary file to the bucket - test_blob.upload_from_string("") - # Clean up the test file - test_blob.delete() - logging.info(f"Write permission confirmed for new path {cloud_path}") - return True - except Forbidden: - logging.error(f"No write permission on path {cloud_path}") - return False - except GoogleAPICallError as e: - logging.error(f"Error accessing path {cloud_path}: {e}") - return False + # Use the existing load_blob_from_full_path method to get the blob + blob = self.load_blob_from_full_path(cloud_path) + blob_exists = blob.exists() - except Exception as e: - logging.warning(f"Failed to check write permission for {cloud_path}: {str(e)}") - return False + if blob_exists: + # Try updating metadata (doesn't change the content) + try: + original_metadata = blob.metadata or {} + test_metadata = original_metadata.copy() + test_metadata["_write_permission_test"] = "true" + + blob.metadata = test_metadata + blob.patch() + + # Restore the original metadata + blob.metadata = original_metadata + blob.patch() + + logging.info(f"Write permission confirmed for existing blob {cloud_path}") + return True + except Forbidden: + logging.error(f"No write permission on existing blob {cloud_path}") + return False + except GoogleAPICallError as e: + logging.error(f"Error accessing blob {cloud_path}: {e}") + return False + else: + # If blob doesn't exist, try to create a temporary zero-byte file + # in the same directory to test write permissions + test_file = f"{cloud_path}.permission_test_temp" + test_blob = self.load_blob_from_full_path(test_file) + + try: + # Try writing a temporary file to the bucket + test_blob.upload_from_string("") + # Clean up the test file + test_blob.delete() + logging.info(f"Write permission confirmed for new path {cloud_path}") + return True + except Forbidden: + logging.error(f"No write permission on path {cloud_path}") + return False + except GoogleAPICallError as e: + logging.error(f"Error accessing path {cloud_path}: {e}") + return False - def wait_for_write_permission(self, cloud_path: str, interval_wait_time_minutes: int = 1, max_wait_time_minutes: int = 30) -> bool: + def wait_for_write_permission(self, cloud_path: str, interval_wait_time_minutes, max_wait_time_minutes) -> bool: """ Wait for write permissions on a GCP path, checking at regular intervals. @@ -826,11 +822,12 @@ def wait_for_write_permission(self, cloud_path: str, interval_wait_time_minutes: **Returns:** - bool: True if write permission is granted within the wait time, False otherwise. """ - import time + if not cloud_path.startswith("gs://"): + raise ValueError("cloud_path must start with 'gs://'") # Convert minutes to seconds for the sleep function - interval_seconds = interval_wait_time_minutes * 60 - max_wait_seconds = max_wait_time_minutes * 60 + interval_seconds = 15#interval_wait_time_minutes * 60 + max_wait_seconds = 40#max_wait_time_minutes * 60 start_time = time.time() attempt_number = 1 From 572174d709cfdddee92834ca01c2df6b5c8872f8 Mon Sep 17 00:00:00 2001 From: Sam Novod Date: Wed, 27 Aug 2025 16:21:55 -0400 Subject: [PATCH 3/9] ... --- ops_utils/gcp_utils.py | 40 ++++++++++------------------------------ 1 file changed, 10 insertions(+), 30 deletions(-) diff --git a/ops_utils/gcp_utils.py b/ops_utils/gcp_utils.py index fa76a0e..ac07d45 100644 --- a/ops_utils/gcp_utils.py +++ b/ops_utils/gcp_utils.py @@ -746,26 +746,11 @@ def has_write_permission(self, cloud_path: str) -> bool: - bool: True if the user has write permission, False otherwise. """ if cloud_path.endswith("/"): - logging.warning(f"Provided cloud path {cloud_path} is a directory, will check {cloud_path}/permission_test_temp") + logging.warning(f"Provided cloud path {cloud_path} is a directory, will check {cloud_path}permission_test_temp") cloud_path = f"{cloud_path}permission_test_temp" - # Process the cloud path to get the bucket and blob name - components = self._process_cloud_path(cloud_path) - bucket_name = components["bucket"] - - # Get the bucket with user_project set for requester pays buckets - bucket = self.client.bucket(bucket_name, user_project=self.client.project) - - # Check if we can access the bucket at all - if not bucket.exists(): - logging.warning(f"Bucket {bucket_name} does not exist or you don't have access to it") - return False - - # Use the existing load_blob_from_full_path method to get the blob blob = self.load_blob_from_full_path(cloud_path) - blob_exists = blob.exists() - - if blob_exists: + if blob.exists(): # Try updating metadata (doesn't change the content) try: original_metadata = blob.metadata or {} @@ -782,29 +767,25 @@ def has_write_permission(self, cloud_path: str) -> bool: logging.info(f"Write permission confirmed for existing blob {cloud_path}") return True except Forbidden: - logging.error(f"No write permission on existing blob {cloud_path}") + logging.warning(f"No write permission on existing blob {cloud_path}") return False except GoogleAPICallError as e: - logging.error(f"Error accessing blob {cloud_path}: {e}") + logging.warning(f"Error accessing blob {cloud_path}: {e}") return False else: - # If blob doesn't exist, try to create a temporary zero-byte file - # in the same directory to test write permissions - test_file = f"{cloud_path}.permission_test_temp" - test_blob = self.load_blob_from_full_path(test_file) - try: # Try writing a temporary file to the bucket - test_blob.upload_from_string("") + blob.upload_from_string("") + # Clean up the test file - test_blob.delete() - logging.info(f"Write permission confirmed for new path {cloud_path}") + blob.delete() + logging.info(f"Write permission confirmed for {cloud_path}") return True except Forbidden: - logging.error(f"No write permission on path {cloud_path}") + logging.warning(f"No write permission on path {cloud_path}") return False except GoogleAPICallError as e: - logging.error(f"Error accessing path {cloud_path}: {e}") + logging.warning(f"Error testing write access to {cloud_path}: {e}") return False def wait_for_write_permission(self, cloud_path: str, interval_wait_time_minutes, max_wait_time_minutes) -> bool: @@ -864,4 +845,3 @@ def wait_for_write_permission(self, cloud_path: str, interval_wait_time_minutes, logging.warning(f"Maximum wait time of {max_wait_time_minutes} minute(s) exceeded. " f"Write permission was not granted for {cloud_path} after {attempt_number} attempts.") return False - From aa2c4822476322403c4d774107ffcfb374930d60 Mon Sep 17 00:00:00 2001 From: Sam Novod Date: Wed, 27 Aug 2025 16:24:08 -0400 Subject: [PATCH 4/9] ... --- ops_utils/gcp_utils.py | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/ops_utils/gcp_utils.py b/ops_utils/gcp_utils.py index ac07d45..b4bc64d 100644 --- a/ops_utils/gcp_utils.py +++ b/ops_utils/gcp_utils.py @@ -750,9 +750,9 @@ def has_write_permission(self, cloud_path: str) -> bool: cloud_path = f"{cloud_path}permission_test_temp" blob = self.load_blob_from_full_path(cloud_path) - if blob.exists(): - # Try updating metadata (doesn't change the content) - try: + try: + if blob.exists(): + # Try updating metadata (doesn't change the content) original_metadata = blob.metadata or {} test_metadata = original_metadata.copy() test_metadata["_write_permission_test"] = "true" @@ -766,14 +766,7 @@ def has_write_permission(self, cloud_path: str) -> bool: logging.info(f"Write permission confirmed for existing blob {cloud_path}") return True - except Forbidden: - logging.warning(f"No write permission on existing blob {cloud_path}") - return False - except GoogleAPICallError as e: - logging.warning(f"Error accessing blob {cloud_path}: {e}") - return False - else: - try: + else: # Try writing a temporary file to the bucket blob.upload_from_string("") @@ -781,12 +774,12 @@ def has_write_permission(self, cloud_path: str) -> bool: blob.delete() logging.info(f"Write permission confirmed for {cloud_path}") return True - except Forbidden: - logging.warning(f"No write permission on path {cloud_path}") - return False - except GoogleAPICallError as e: - logging.warning(f"Error testing write access to {cloud_path}: {e}") - return False + except Forbidden: + logging.warning(f"No write permission on path {cloud_path}") + return False + except GoogleAPICallError as e: + logging.warning(f"Error testing write access to {cloud_path}: {e}") + return False def wait_for_write_permission(self, cloud_path: str, interval_wait_time_minutes, max_wait_time_minutes) -> bool: """ From c995f5b5d76c25bf387624ff29092573193946b6 Mon Sep 17 00:00:00 2001 From: Sam Novod Date: Wed, 27 Aug 2025 16:25:23 -0400 Subject: [PATCH 5/9] ... --- ops_utils/gcp_utils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ops_utils/gcp_utils.py b/ops_utils/gcp_utils.py index b4bc64d..b37da73 100644 --- a/ops_utils/gcp_utils.py +++ b/ops_utils/gcp_utils.py @@ -745,12 +745,13 @@ def has_write_permission(self, cloud_path: str) -> bool: **Returns:** - bool: True if the user has write permission, False otherwise. """ + if not cloud_path.startswith("gs://"): + raise ValueError("cloud_path must start with 'gs://'") if cloud_path.endswith("/"): logging.warning(f"Provided cloud path {cloud_path} is a directory, will check {cloud_path}permission_test_temp") cloud_path = f"{cloud_path}permission_test_temp" - - blob = self.load_blob_from_full_path(cloud_path) try: + blob = self.load_blob_from_full_path(cloud_path) if blob.exists(): # Try updating metadata (doesn't change the content) original_metadata = blob.metadata or {} From a57a4cb41e515cab3be02fe323e3a127e415d10e Mon Sep 17 00:00:00 2001 From: Sam Novod Date: Wed, 27 Aug 2025 16:29:28 -0400 Subject: [PATCH 6/9] ... --- ops_utils/gcp_utils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ops_utils/gcp_utils.py b/ops_utils/gcp_utils.py index b37da73..be32e73 100644 --- a/ops_utils/gcp_utils.py +++ b/ops_utils/gcp_utils.py @@ -807,8 +807,9 @@ def wait_for_write_permission(self, cloud_path: str, interval_wait_time_minutes, start_time = time.time() attempt_number = 1 - logging.info(f"Starting to check for write permissions on {cloud_path}") - logging.info(f"Will check every {interval_wait_time_minutes} minute(s) for up to {max_wait_time_minutes} minute(s)") + logging.info( + f"Starting to check for write permissions on {cloud_path}. Will check " + f"every {interval_wait_time_minutes} minute(s) for up to {max_wait_time_minutes} minute(s).") # First check immediately if self.has_write_permission(cloud_path): From 59a5fdb7ab3eea1d54d4385e24b73ba650fd622f Mon Sep 17 00:00:00 2001 From: Sam Novod Date: Wed, 27 Aug 2025 16:48:24 -0400 Subject: [PATCH 7/9] ... --- ops_utils/gcp_utils.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ops_utils/gcp_utils.py b/ops_utils/gcp_utils.py index be32e73..06dd7df 100644 --- a/ops_utils/gcp_utils.py +++ b/ops_utils/gcp_utils.py @@ -782,7 +782,7 @@ def has_write_permission(self, cloud_path: str) -> bool: logging.warning(f"Error testing write access to {cloud_path}: {e}") return False - def wait_for_write_permission(self, cloud_path: str, interval_wait_time_minutes, max_wait_time_minutes) -> bool: + def wait_for_write_permission(self, cloud_path: str, interval_wait_time_minutes: int, max_wait_time_minutes: int) -> bool: """ Wait for write permissions on a GCP path, checking at regular intervals. @@ -791,8 +791,8 @@ def wait_for_write_permission(self, cloud_path: str, interval_wait_time_minutes, **Args:** - cloud_path (str): The GCS path to check for write permissions. - - interval_wait_time_minutes (int, optional): Time in minutes to wait between permission checks. Defaults to 1 minute. - - max_wait_time_minutes (int, optional): Maximum time in minutes to wait for permissions. Defaults to 30 minutes. + - interval_wait_time_minutes (int): Time in minutes to wait between permission checks. + - max_wait_time_minutes (int): Maximum time in minutes to wait for permissions. **Returns:** - bool: True if write permission is granted within the wait time, False otherwise. @@ -801,8 +801,8 @@ def wait_for_write_permission(self, cloud_path: str, interval_wait_time_minutes, raise ValueError("cloud_path must start with 'gs://'") # Convert minutes to seconds for the sleep function - interval_seconds = 15#interval_wait_time_minutes * 60 - max_wait_seconds = 40#max_wait_time_minutes * 60 + interval_seconds = interval_wait_time_minutes * 60 + max_wait_seconds = max_wait_time_minutes * 60 start_time = time.time() attempt_number = 1 From 4b372981b5796d7acc99ba2e516e968bf0a3bdeb Mon Sep 17 00:00:00 2001 From: Sam Novod Date: Thu, 28 Aug 2025 09:17:45 -0400 Subject: [PATCH 8/9] do not return boolean for wait_for_permissions --- ops_utils/gcp_utils.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ops_utils/gcp_utils.py b/ops_utils/gcp_utils.py index 06dd7df..a0c7088 100644 --- a/ops_utils/gcp_utils.py +++ b/ops_utils/gcp_utils.py @@ -782,7 +782,7 @@ def has_write_permission(self, cloud_path: str) -> bool: logging.warning(f"Error testing write access to {cloud_path}: {e}") return False - def wait_for_write_permission(self, cloud_path: str, interval_wait_time_minutes: int, max_wait_time_minutes: int) -> bool: + def wait_for_write_permission(self, cloud_path: str, interval_wait_time_minutes: int, max_wait_time_minutes: int) -> None: """ Wait for write permissions on a GCP path, checking at regular intervals. @@ -814,7 +814,7 @@ def wait_for_write_permission(self, cloud_path: str, interval_wait_time_minutes: # First check immediately if self.has_write_permission(cloud_path): logging.info(f"Write permission confirmed on initial check for {cloud_path}") - return True + return # If first check fails, start periodic checks while time.time() - start_time < max_wait_seconds: @@ -834,9 +834,9 @@ def wait_for_write_permission(self, cloud_path: str, interval_wait_time_minutes: if self.has_write_permission(cloud_path): elapsed_minutes = (time.time() - start_time) / 60 logging.info(f"Write permission confirmed after {elapsed_minutes:.1f} minute(s) on attempt {attempt_number}") - return True + return # If we get here, we've exceeded the maximum wait time - logging.warning(f"Maximum wait time of {max_wait_time_minutes} minute(s) exceeded. " - f"Write permission was not granted for {cloud_path} after {attempt_number} attempts.") - return False + raise PermissionError( + f"Maximum wait time of {max_wait_time_minutes} minute(s) exceeded. Write permission was not granted for " + f"{cloud_path} after {attempt_number} attempts.") From f1fb285d484adb397155a7f52bb38fd6cc519ad2 Mon Sep 17 00:00:00 2001 From: Sam Novod Date: Thu, 28 Aug 2025 10:17:05 -0400 Subject: [PATCH 9/9] testing out lower jira version --- pyproject.toml | 2 +- requirements-dev.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ce99d6b..616f7e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ dependencies = [ "google-cloud-secret-manager", "azure-identity==1.17.1", "azure-storage-blob==12.21.0", - "jira", + "jira==3.8.0", "oauth2client", "backoff", "aiohttp", diff --git a/requirements-dev.txt b/requirements-dev.txt index 5925270..c327be4 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -11,7 +11,7 @@ google-cloud-storage==2.17.0 google-cloud-bigquery google-api-python-client google-cloud-secret-manager -jira +jira==3.8.0 oauth2client numpy pandas