Source code for libcloud.pricing

# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License.  You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import with_statement

"""
A class which handles loading the pricing files.
"""

import os.path
from os.path import join as pjoin

try:
    import simplejson as json
    try:
        JSONDecodeError = json.JSONDecodeError
    except AttributeError:
        # simplejson < 2.1.0 does not have the JSONDecodeError exception class
        JSONDecodeError = ValueError
except ImportError:
    import json
    JSONDecodeError = ValueError

from libcloud.utils.connection import get_response_object

__all__ = [
    'get_pricing',
    'get_size_price',
    'set_pricing',
    'clear_pricing_data',
    'download_pricing_file'
]

# Default URL to the pricing file
DEFAULT_FILE_URL = 'https://git-wip-us.apache.org/repos/asf?p=libcloud.git;a=blob_plain;f=libcloud/data/pricing.json'  # NOQA

CURRENT_DIRECTORY = os.path.dirname(os.path.abspath(__file__))
DEFAULT_PRICING_FILE_PATH = pjoin(CURRENT_DIRECTORY, 'data/pricing.json')
CUSTOM_PRICING_FILE_PATH = os.path.expanduser('~/.libcloud/pricing.json')

# Pricing data cache
PRICING_DATA = {
    'compute': {},
    'storage': {}
}

VALID_PRICING_DRIVER_TYPES = ['compute', 'storage']


def get_pricing_file_path(file_path=None):
    if os.path.exists(CUSTOM_PRICING_FILE_PATH) and \
       os.path.isfile(CUSTOM_PRICING_FILE_PATH):
        # Custom pricing file is available, use it
        return CUSTOM_PRICING_FILE_PATH

    return DEFAULT_PRICING_FILE_PATH


[docs]def get_pricing(driver_type, driver_name, pricing_file_path=None): """ Return pricing for the provided driver. :type driver_type: ``str`` :param driver_type: Driver type ('compute' or 'storage') :type driver_name: ``str`` :param driver_name: Driver name :type pricing_file_path: ``str`` :param pricing_file_path: Custom path to a price file. If not provided it uses a default path. :rtype: ``dict`` :return: Dictionary with pricing where a key name is size ID and the value is a price. """ if driver_type not in VALID_PRICING_DRIVER_TYPES: raise AttributeError('Invalid driver type: %s', driver_type) if driver_name in PRICING_DATA[driver_type]: return PRICING_DATA[driver_type][driver_name] if not pricing_file_path: pricing_file_path = get_pricing_file_path(file_path=pricing_file_path) with open(pricing_file_path) as fp: content = fp.read() pricing_data = json.loads(content) size_pricing = pricing_data[driver_type][driver_name] for driver_type in VALID_PRICING_DRIVER_TYPES: # pylint: disable=maybe-no-member pricing = pricing_data.get(driver_type, None) if pricing: PRICING_DATA[driver_type] = pricing return size_pricing
[docs]def set_pricing(driver_type, driver_name, pricing): """ Populate the driver pricing dictionary. :type driver_type: ``str`` :param driver_type: Driver type ('compute' or 'storage') :type driver_name: ``str`` :param driver_name: Driver name :type pricing: ``dict`` :param pricing: Dictionary where a key is a size ID and a value is a price. """ PRICING_DATA[driver_type][driver_name] = pricing
[docs]def get_size_price(driver_type, driver_name, size_id): """ Return price for the provided size. :type driver_type: ``str`` :param driver_type: Driver type ('compute' or 'storage') :type driver_name: ``str`` :param driver_name: Driver name :type size_id: ``str`` or ``int`` :param size_id: Unique size ID (can be an integer or a string - depends on the driver) :rtype: ``float`` :return: Size price. """ pricing = get_pricing(driver_type=driver_type, driver_name=driver_name) try: price = float(pricing[size_id]) except KeyError: # Price not available price = None return price
def invalidate_pricing_cache(): """ Invalidate pricing cache for all the drivers. """ PRICING_DATA['compute'] = {} PRICING_DATA['storage'] = {}
[docs]def clear_pricing_data(): """ Invalidate pricing cache for all the drivers. Note: This method does the same thing as invalidate_pricing_cache and is here for backward compatibility reasons. """ invalidate_pricing_cache()
def invalidate_module_pricing_cache(driver_type, driver_name): """ Invalidate the cache for the specified driver. :type driver_type: ``str`` :param driver_type: Driver type ('compute' or 'storage') :type driver_name: ``str`` :param driver_name: Driver name """ if driver_name in PRICING_DATA[driver_type]: del PRICING_DATA[driver_type][driver_name]
[docs]def download_pricing_file(file_url=DEFAULT_FILE_URL, file_path=CUSTOM_PRICING_FILE_PATH): """ Download pricing file from the file_url and save it to file_path. :type file_url: ``str`` :param file_url: URL pointing to the pricing file. :type file_path: ``str`` :param file_path: Path where a download pricing file will be saved. """ dir_name = os.path.dirname(file_path) if not os.path.exists(dir_name): # Verify a valid path is provided msg = ('Can\'t write to %s, directory %s, doesn\'t exist' % (file_path, dir_name)) raise ValueError(msg) if os.path.exists(file_path) and os.path.isdir(file_path): msg = ('Can\'t write to %s file path because it\'s a' ' directory' % (file_path)) raise ValueError(msg) response = get_response_object(file_url) body = response.body # Verify pricing file is valid try: data = json.loads(body) except JSONDecodeError: msg = 'Provided URL doesn\'t contain valid pricing data' raise Exception(msg) # pylint: disable=maybe-no-member if not data.get('updated', None): msg = 'Provided URL doesn\'t contain valid pricing data' raise Exception(msg) # No need to stream it since file is small with open(file_path, 'w') as file_handle: file_handle.write(body)