Skip to content
4 changes: 2 additions & 2 deletions blackduck/Exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
'''
import logging
from json import JSONDecodeError
from .Utils import pfmt
from pprint import pformat

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -39,7 +39,7 @@ def http_exception_handler(self, response, name):
}

try:
content = pfmt(response.json())
content = pformat(response.json())
except JSONDecodeError:
content = response.text

Expand Down
66 changes: 20 additions & 46 deletions blackduck/Utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,38 +14,35 @@
logger = logging.getLogger(__name__)


def iso8601_to_date(iso_string, with_zone=False):
"""Utility function to convert iso_8601 formatted string to datetime object, optionally accounting for timezone
def to_datetime(date):
"""Utility function to convert common date formats to datetime object

Args:
iso_string (string): the iso_8601 string to convert to datetime object
with_zone (bool, optional): whether to account for timezone offset. Defaults to False.
iso_string (string/datetime/date)

Returns:
datetime.datetime: equivalent time, with or without timezone offsets
datetime.datetime
"""
date_timezone = iso_string.split('Z')
date = dateutil.parser.parse(date_timezone[0])
if with_zone and len(date_timezone > 1):
hours_minutes = date_timezone[1].split(':')
minutes = (60*int(hours_minutes[0]) + int(hours_minutes[1] if len(hours_minutes) > 1 else 0))
date = date + datetime.timedelta(minutes=minutes)
return date

def iso8601_timespan(days_ago, from_date=datetime.utcnow(), delta=timedelta(weeks=1)):
if isinstance(date, str):
from dateutil.parser import parse
return parse(date)
if isinstance(date, datetime.datetime):
return date
if isinstance(date, datetime.date):
return datetime.datetime(
year=date.year,
month=date.month,
day=date.day
)
raise TypeError(f"object of type {type(date)} cannot be converted to datetime")


def timespan(days_ago, from_date=datetime.now(), delta=timedelta(weeks=1)):
curr_date = from_date - timedelta(days=days_ago)
while curr_date < from_date:
yield curr_date.isoformat('T', 'seconds')
yield curr_date
curr_date += delta

def min_iso8601():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I am all for deleting code, are these methods that you just recently added e.g. this year? Generally, deleting or changing method signatures are considered as breaking backwards-compatibility in an API.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None of these were in HubInstance - the only use they had was in demo_client, though I'll update that to use the two new methods.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay I'm fine with deleting them.

"""Utility wrapper for iso8601_to_date which provides minimum date (for comparison purposes).

Returns:
datetime.datetime: 0 / 1970-01-01T00:00:00.000
"""
return iso8601_to_date("1970-01-01T00:00:00.000")

def find_field(data_to_filter, field_name, field_value):
"""Utility function to filter blackduck objects for specific fields

Expand Down Expand Up @@ -104,29 +101,6 @@ def get_resource_name(obj):
if re.search("^\w{8}-\w{4}-\w{4}-\w{4}-\w{12}$", part):
continue
return part


def pfmt(value):
"""Utility function to 'pretty format' a dict or json

Args:
value (json/dict): the json object or dict to pretty format

Returns:
string: json formatted string representing passed object
"""
return json.dumps(value, indent=4)

def pprint(value):
"""Utility wrapper for pfmt that prints 'pretty formatted' json data.

Args:
value (json/dict): the json object or dict to pretty print

Returns:
None
"""
print(pfmt(value))

def object_id(object):
assert '_meta' in object, "REST API object must have _meta key"
Expand Down
2 changes: 1 addition & 1 deletion blackduck/__version__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
VERSION = (1, 0, 2)
VERSION = (1, 0, 3)

__version__ = '.'.join(map(str, VERSION))
52 changes: 52 additions & 0 deletions examples/client/delete_empty_project_versions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from blackduck import Client
from blackduck.Utils import to_datetime

from datetime import datetime, timedelta
import argparse
import logging
import requests

logging.basicConfig(
level=logging.INFO,
format="[%(asctime)s] {%(module)s:%(lineno)d} %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)

parser = argparse.ArgumentParser("delete_empty_project_versions")
parser.add_argument("--base-url", required=True, help="blackduck hub server url e.g. https://your.app.blackduck.com")
parser.add_argument("--token-file", dest='token_file', required=True, help="path to file containing blackduck hub token")
parser.add_argument("--no-verify", dest='verify', action='store_false', help="disable TLS certificate verification")
parser.add_argument("--days", dest='days', default=30, type=int, help="projects/versions older than <days>")
parser.add_argument("--delete", dest='delete', action='store_true', help="without this flag script will log/print only")
args = parser.parse_args()

with open(args.token_file, 'r') as tf:
access_token = tf.readline().strip()

bd = Client(
base_url=args.base_url,
token=access_token,
verify=args.verify
)

max_age = datetime.now() - timedelta(days=args.days)

for project in bd.get_resource('projects'):
# skip projects younger than max age
if to_datetime(project.get('createdAt')) > max_age: continue
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm getting an exception here: TypeError: can't compare offset-naive and offset-aware datetimes

Copy link
Collaborator Author

@OffBy0x01 OffBy0x01 May 18, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A possible solution could be to add something like this to utils:

// from dateutil.tz import tzlocal def to_local_datetime(date): return to_datetime(date).astimezone(tz=tzlocal()) def get_local_datetime(): return datetime.now(tz=tzlocal()) 

Normally I'd avoid creating wrappers for stl functions but in this case I don't think it is obvious how to get a timezone aware datetime given datetime.now() and even datetime.utcnow() are timezone naive. Thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In sage_version_activity_to_csv.py I used: from dateutil.parser import isoparse
Would that work?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll take a look at that tonight


for version in bd.get_resource('versions', project):
# skip versions with > 0 code locations
if int(bd.get_metadata('codelocations', version).get('totalCount')): continue
# skip versions younger than max age
if to_datetime(version.get('createdAt')) > max_age: continue
# delete all others
logger.info(f"delete {project.get('name')} version {version.get('versionName')}")
if args.delete: bd.session.delete(version.get('href')).raise_for_status()

# skip projects with any remaining versions
if int(bd.get_metadata('versions', project).get('totalCount')): continue
# delete all others
logger.info(f"deleting {project.get('name')}")
if args.delete: bd.session.delete(project.get('href')).raise_for_status()