def __init__(self, config=None, provider_name=None, network_timeout=60 * 10, **kwargs): """ Constructor Authenticates a provider client for the given domain. @param config: parselmouth.config.ParselmouthConfig @param provider_name: any(parselmouth.constants.ParseltoungProvider) @param network_timeout: int, number seconds before timing out a request the given ad provider service """ self._network_timeout = network_timeout self.provider_config = config # Load the provider configuration if self.provider_config and not isinstance(self.provider_config, ParselmouthConfig): raise ParselmouthException( "Invalid config. Config should be of type ParselmouthConfig", ) elif not self.provider_config: self.provider_config = ParselmouthConfig(provider_name, **kwargs) # Get interface for given provider self.provider_name = self.provider_config.provider_name provider_interface_class = self.get_ad_service_interface_for_provider( self.provider_name ) self.provider = provider_interface_class( **self.provider_config.get_credentials_arguments() ) self.tree_builder = TreeBuilder( self.provider_name, self.provider, ) # Attempt to access the network to check proper configuration try: self.get_network_timezone() except Exception as e: raise ParselmouthException( "Provider not configured correctly. Got error: '{}'".format( str(e) ) )
class TreeBuilderTest(unittest.TestCase): def setUp(self): self.tree_builder = TreeBuilder(None, None) def test_build_tree(self): # Test empty tree nodes1 = [] test1 = self.tree_builder.build_tree(nodes1) self.assertEqual(test1, EMPTY_TREE) # Test basic nesting test2 = self.tree_builder.build_tree(NESTED_NODES) self.assertEqual(test2, NESTED_TREE) # Test disjoint branches test3 = self.tree_builder.build_tree(DISJOINT_NODES) self.assertEqual(test3, DISJOINT_TREE) # Test shared ancestor test3 = self.tree_builder.build_tree(ANCESTOR_NODES) self.assertEqual(test3, ANCESTOR_TREE) def test_doc_conversion(self): doc1 = self.tree_builder._convert_node_tree_to_doc(NESTED_TREE) test1 = self.tree_builder._convert_doc_to_node_tree( doc1, ParselmouthTargetTypes.adunit, ) self.assertEqual(test1, NESTED_TREE) doc2 = self.tree_builder._convert_node_tree_to_doc(DISJOINT_TREE) test2 = self.tree_builder._convert_doc_to_node_tree( doc2, ParselmouthTargetTypes.adunit, ) self.assertEqual(test2, DISJOINT_TREE) doc3 = self.tree_builder._convert_node_tree_to_doc(ANCESTOR_TREE) test3 = self.tree_builder._convert_doc_to_node_tree( doc3, ParselmouthTargetTypes.adunit, ) self.assertEqual(test3, ANCESTOR_TREE)
class Parselmouth(object): """ Base Interface Class """ ProviderInterfaceMap = { ParselmouthProviders.google_dfp_premium: DFPInterface, ParselmouthProviders.google_dfp_small_business: DFPInterface, } """ dict, mapping between ad service providers and their implementations NOTES: * This should probably be hosted elsewhere... * This doesn't serve much purpose right now since all we support is DFP... """ def __init__(self, config=None, provider_name=None, network_timeout=60 * 10, **kwargs): """ Constructor Authenticates a provider client for the given domain. @param config: parselmouth.config.ParselmouthConfig @param provider_name: any(parselmouth.constants.ParseltoungProvider) @param network_timeout: int, number seconds before timing out a request the given ad provider service """ self._network_timeout = network_timeout self.provider_config = config # Load the provider configuration if self.provider_config and not isinstance(self.provider_config, ParselmouthConfig): raise ParselmouthException( "Invalid config. Config should be of type ParselmouthConfig", ) elif not self.provider_config: self.provider_config = ParselmouthConfig(provider_name, **kwargs) # Get interface for given provider self.provider_name = self.provider_config.provider_name provider_interface_class = self.get_ad_service_interface_for_provider( self.provider_name ) self.provider = provider_interface_class( **self.provider_config.get_credentials_arguments() ) self.tree_builder = TreeBuilder( self.provider_name, self.provider, ) # Attempt to access the network to check proper configuration try: self.get_network_timezone() except Exception as e: raise ParselmouthException("Provider not configured correctly. {}".format(str(e))) def __str__(self): """ Human readable representation of this object @return: str """ return ( "{class_name}(" "provider={provider}," "provider_config={provider_config}" ")" ).format( class_name=self.__class__.__name__, provider=self.provider, provider_config=self.provider_config, ) @classmethod def get_ad_service_interface_for_provider(cls, provider_name): """ Staticmethod, returns the interface class for a given ad service provider name @param provider_name: str, one of enum parselmouth.constants.ParselmouthProviders @return: descendant of parselmouth.adapters.abstract_interface.AbstractInterface """ client_interface = cls.ProviderInterfaceMap.get(provider_name) if not client_interface: raise ValueError( "There is no interface defined for provider: %s" % provider_name ) return client_interface @classmethod def get_ad_service_config_interface_for_provider(cls, provider_name): """ Staticmethod, returns the config interface for a given ad service provider name @param provider_name: str, one of enum parselmouth.constants.ParselmouthProviders @return: child(parselmouth.config.ParselmouthConfig) """ config_interface = cls.ProviderConfigInterfaceMap.get(provider_name) if not config_interface: raise ValueError( "There is no config interface defined for provider: %s" % provider_name ) return config_interface def get_network_timezone(self): """ Get the DFP network timezone for the host this client is connected to @return: pytz.timezone """ with Timeout(self._network_timeout): return self.provider.get_network_timezone() def get_advertisers(self): """ Queries dfp for all advertisers within their account @return: list(dict), returns a list of company dictionaries """ with Timeout(self._network_timeout): return self.provider.get_advertisers() def get_campaign(self, campaign_id, include_line_items=False): """ Return a campaign object given an id @param campaign_id: str, id of the campaign to return @param include_line_items: bool, include line item data as well @return: parselmouth.delivery.Campaign """ with Timeout(self._network_timeout): campaign = self.provider.get_campaign(campaign_id) if include_line_items: campaign.line_items = self.get_campaign_line_items(campaign) return campaign def get_campaigns(self, **kwargs): """ Get campaigns on optional filters @param order: str, PQL key to sort on (default=ID) @param limit: int, number of PQL results to return @param offset: int, page in a stream of PQL results to return @param filter_kwargs: dict, keyword arguments on which to filter PQL results @return: L{parselmouth.delivery.Campaign} """ with Timeout(self._network_timeout): return self.provider.get_campaigns(**kwargs) def get_line_item(self, line_item_id): """ Return a line item object given an id @param line_item_id: str, id of the LineItem to return @return: parselmouth.delivery.LineItem """ attempt = 1 response = None while not response and attempt <= MAX_REQUEST_ATTEMPTS: try: with Timeout(self._network_timeout): response = self.provider.get_line_item(line_item_id) except ParselmouthNetworkError: logging.exception("Got network error on attempt %s" % attempt) attempt += 1 if not response: raise ParselmouthException( 'Could not fetch a line item in {0} attempts'.format( MAX_REQUEST_ATTEMPTS ) ) return response def get_line_items(self, **kwargs): """ Get line items on optional filters @param order: str, PQL key to sort on (default=ID) @param limit: int, number of PQL results to return @param offset: int, page in a stream of PQL results to return @param filter_kwargs: dict, keyword arguments on which to filter PQL results @return: L{parselmouth.delivery.LineItem} """ with Timeout(self._network_timeout): return self.provider.get_line_items(**kwargs) def get_campaign_line_items(self, campaign): """ Get line items on optional filters @param campaign: Campaign|str, @return: L{parselmouth.delivery.LineItem} """ with Timeout(self._network_timeout): return self.provider.get_campaign_line_items(campaign) def get_line_item_available_inventory(self, line_item, use_start=False, preserve_id=False): """ Get number of impressions available for line item. NOTE: The following fields are required within a line item to perform an availability check: type, cost_type, end, targeting.inventory @param line_item: LineItem @param use_start: bool, if False, checks availability from right now @param preserve_id: bool, communicate the id of the line item being forecasted to DFP. This is used in the case where we want to get a forecast on a line item in flight. This way, a domain doesn't need to have enough spare inventory to accommodate the two line items simultaneously. NOTE: If this is true then use_start is necessarily true. @return: int|None, number of available impressions """ with Timeout(self._network_timeout): return self.provider.get_line_item_available_inventory( line_item, use_start, preserve_id ) def get_creative(self, creative_id): """ Return a creative object given an id @param creative_id: str, id of the campaign to return @return: parselmouth.delivery.Creative """ return self.provider.get_creative(creative_id) def get_creatives(self, **kwargs): """ Get creatives on optional filters @param order: str, PQL key to sort on (default=ID) @param limit: int, number of PQL results to return @param offset: int, page in a stream of PQL results to return @param filter_kwargs: dict, keyword arguments on which to filter PQL results @return: L{parselmouth.delivery.Creative} """ with Timeout(self._network_timeout): return self.provider.get_creatives(**kwargs) def get_line_item_creatives(self, line_item): """ Return the creatives associated with a given line item @param line_item: str|int|parselmouth.delivery.LineItem, either the id of the lineitem or an object with the id @return: list(parselmouth.delivery.Creative) """ with Timeout(self._network_timeout): return self.provider.get_line_item_creatives(line_item) def get_line_item_report(self, start, end, columns=[ParselmouthReportMetrics.ad_impressions]): """ Get the number of impressions served by DFP for all line items between the two datetimes. Note: These datetimes are assumed to be in the timezone of the host and will be aligned to day Note: This API has a strange behavior where the date inputs only allow for a calendar day. This means start=1/1, end=1/1 will query against the entire day of 1/1 @param start: datetime, @param end: datetime, @param columns: list(str), list of columns to include @return: list(dict) """ with Timeout(self._network_timeout): return self.provider.get_line_item_report( start, end, columns, ) def get_custom_target_by_name(self, name, parent_name): """ Get a custom target by its name @param name: name of custom target @param parent_name: name of parent target @return: Custom|None """ with Timeout(self._network_timeout): targets = self.provider.get_custom_targets( key_name=parent_name, value_name=name, ) if targets and len(targets) > 1: raise ParselmouthException('Given name is not unique') elif targets: return targets[0] else: return None def construct_tree(self, target_type): """ Get all data of type target_type from ad provider, and build a tree @param taget_type: parselmouthTargetTypes @return: NodeTree """ return self.tree_builder.construct_tree(target_type) def update_line_item(self, line_item): """ Update a Line Item for a provider @param line_item: parselmouth.delivery.LineItem """ with Timeout(self._network_timeout): self.update_line_items([line_item]) def update_line_items(self, line_items): """ Update multiple Line Item for a provider @param line_items: L{parselmouth.delivery.LineItem} """ with Timeout(self._network_timeout): self.provider.update_line_items(line_items) def create_custom_target(self, key, value): """ Add a custom target to this domain's ad service provider @param key: parselmouth.targeting.Custom @param value: parselmouth.targeting.Custom @return: list(parselmouth.targeting.Custom) """ with Timeout(self._network_timeout): return self.provider.create_custom_target(key, value)
class Parselmouth(object): """ Base Interface Class """ ProviderInterfaceMap = { ParselmouthProviders.google_dfp_premium: DFPInterface, ParselmouthProviders.google_dfp_small_business: DFPInterface, } """ dict, mapping between ad service providers and their implementations NOTES: * This should probably be hosted elsewhere... * This doesn't serve much purpose right now since all we support is DFP... """ def __init__(self, config=None, provider_name=None, network_timeout=60 * 10, **kwargs): """ Constructor Authenticates a provider client for the given domain. @param config: parselmouth.config.ParselmouthConfig @param provider_name: any(parselmouth.constants.ParseltoungProvider) @param network_timeout: int, number seconds before timing out a request the given ad provider service """ self._network_timeout = network_timeout self.provider_config = config # Load the provider configuration if self.provider_config and not isinstance(self.provider_config, ParselmouthConfig): raise ParselmouthException( "Invalid config. Config should be of type ParselmouthConfig", ) elif not self.provider_config: self.provider_config = ParselmouthConfig(provider_name, **kwargs) # Get interface for given provider self.provider_name = self.provider_config.provider_name provider_interface_class = self.get_ad_service_interface_for_provider( self.provider_name ) self.provider = provider_interface_class( **self.provider_config.get_credentials_arguments() ) self.tree_builder = TreeBuilder( self.provider_name, self.provider, ) # Attempt to access the network to check proper configuration try: self.get_network_timezone() except Exception as e: raise ParselmouthException( "Provider not configured correctly. Got error: '{}'".format( str(e) ) ) def __str__(self): """ Human readable representation of this object @return: str """ return ( "{class_name}(" "provider={provider}," "provider_config={provider_config}" ")" ).format( class_name=self.__class__.__name__, provider=self.provider, provider_config=self.provider_config, ) @classmethod def get_ad_service_interface_for_provider(cls, provider_name): """ Staticmethod, returns the interface class for a given ad service provider name @param provider_name: str, one of enum parselmouth.constants.ParselmouthProviders @return: descendant of parselmouth.adapters.abstract_interface.AbstractInterface """ client_interface = cls.ProviderInterfaceMap.get(provider_name) if not client_interface: raise ValueError( "There is no interface defined for provider: %s" % provider_name ) return client_interface @classmethod def get_ad_service_config_interface_for_provider(cls, provider_name): """ Staticmethod, returns the config interface for a given ad service provider name @param provider_name: str, one of enum parselmouth.constants.ParselmouthProviders @return: child(parselmouth.config.ParselmouthConfig) """ config_interface = cls.ProviderConfigInterfaceMap.get(provider_name) if not config_interface: raise ValueError( "There is no config interface defined for provider: %s" % provider_name ) return config_interface def get_network_timezone(self): """ Get the DFP network timezone for the host this client is connected to @return: pytz.timezone """ with Timeout(self._network_timeout): return self.provider.get_network_timezone() def get_advertisers(self): """ Queries dfp for all advertisers within their account @return: list(dict), returns a list of company dictionaries """ with Timeout(self._network_timeout): return self.provider.get_advertisers() def get_campaign(self, campaign_id, include_line_items=False): """ Return a campaign object given an id @param campaign_id: str, id of the campaign to return @param include_line_items: bool, include line item data as well @return: parselmouth.delivery.Campaign """ with Timeout(self._network_timeout): campaign = self.provider.get_campaign(campaign_id) if include_line_items: campaign.line_items = self.get_campaign_line_items(campaign) return campaign def get_campaigns(self, **kwargs): """ Get campaigns on optional filters @param order: str, PQL key to sort on (default=ID) @param limit: int, number of PQL results to return @param offset: int, page in a stream of PQL results to return @param filter_kwargs: dict, keyword arguments on which to filter PQL results @return: L{parselmouth.delivery.Campaign} """ with Timeout(self._network_timeout): return self.provider.get_campaigns(**kwargs) def get_line_item(self, line_item_id): """ Return a line item object given an id @param line_item_id: str, id of the LineItem to return @return: parselmouth.delivery.LineItem """ attempt = 1 response = None while not response and attempt <= MAX_REQUEST_ATTEMPTS: try: with Timeout(self._network_timeout): response = self.provider.get_line_item(line_item_id) except ParselmouthNetworkError: logging.exception("Got network error on attempt %s" % attempt) attempt += 1 if not response: raise ParselmouthException( 'Could not fetch a line item in {0} attempts'.format( MAX_REQUEST_ATTEMPTS ) ) return response def get_line_items(self, **kwargs): """ Get line items on optional filters @param order: str, PQL key to sort on (default=ID) @param limit: int, number of PQL results to return @param offset: int, page in a stream of PQL results to return @param filter_kwargs: dict, keyword arguments on which to filter PQL results @return: L{parselmouth.delivery.LineItem} """ with Timeout(self._network_timeout): return self.provider.get_line_items(**kwargs) def get_campaign_line_items(self, campaign): """ Get line items on optional filters @param campaign: Campaign|str, @return: L{parselmouth.delivery.LineItem} """ with Timeout(self._network_timeout): return self.provider.get_campaign_line_items(campaign) def get_line_item_available_inventory(self, line_item, use_start=False, preserve_id=False): """ Get number of impressions available for line item. NOTE: The following fields are required within a line item to perform an availability check: type, cost_type, end, targeting.inventory @param line_item: LineItem @param use_start: bool, if False, checks availability from right now @param preserve_id: bool, communicate the id of the line item being forecasted to DFP. This is used in the case where we want to get a forecast on a line item in flight. This way, a domain doesn't need to have enough spare inventory to accommodate the two line items simultaneously. NOTE: If this is true then use_start is necessarily true. @return: int|None, number of available impressions """ with Timeout(self._network_timeout): return self.provider.get_line_item_available_inventory( line_item, use_start, preserve_id ) def get_creative(self, creative_id): """ Return a creative object given an id @param creative_id: str, id of the campaign to return @return: parselmouth.delivery.Creative """ return self.provider.get_creative(creative_id) def get_creatives(self, **kwargs): """ Get creatives on optional filters @param order: str, PQL key to sort on (default=ID) @param limit: int, number of PQL results to return @param offset: int, page in a stream of PQL results to return @param filter_kwargs: dict, keyword arguments on which to filter PQL results @return: L{parselmouth.delivery.Creative} """ with Timeout(self._network_timeout): return self.provider.get_creatives(**kwargs) def get_line_item_creatives(self, line_item): """ Return the creatives associated with a given line item @param line_item: str|int|parselmouth.delivery.LineItem, either the id of the lineitem or an object with the id @return: list(parselmouth.delivery.Creative) """ with Timeout(self._network_timeout): return self.provider.get_line_item_creatives(line_item) def get_line_item_report(self, start, end, columns=[ParselmouthReportMetrics.ad_impressions]): """ Get the number of impressions served by DFP for all line items between the two datetimes. Note: These datetimes are assumed to be in the timezone of the host and will be aligned to day Note: This API has a strange behavior where the date inputs only allow for a calendar day. This means start=1/1, end=1/1 will query against the entire day of 1/1 @param start: datetime, @param end: datetime, @param columns: list(str), list of columns to include @return: list(dict) """ with Timeout(self._network_timeout): return self.provider.get_line_item_report( start, end, columns, ) def get_custom_target_by_name(self, name, parent_name): """ Get a custom target by its name @param name: name of custom target @param parent_name: name of parent target @return: Custom|None """ with Timeout(self._network_timeout): targets = self.provider.get_custom_targets( key_name=parent_name, value_name=name, ) if targets and len(targets) > 1: raise ParselmouthException('Given name is not unique') elif targets: return targets[0] else: return None def construct_tree(self, target_type): """ Get all data of type target_type from ad provider, and build a tree @param taget_type: parselmouthTargetTypes @return: NodeTree """ return self.tree_builder.construct_tree(target_type) def update_line_item(self, line_item): """ Update a Line Item for a provider @param line_item: parselmouth.delivery.LineItem """ with Timeout(self._network_timeout): self.update_line_items([line_item]) def update_line_items(self, line_items): """ Update multiple Line Item for a provider @param line_items: L{parselmouth.delivery.LineItem} """ with Timeout(self._network_timeout): self.provider.update_line_items(line_items) def create_custom_target(self, key, value): """ Add a custom target to this domain's ad service provider @param key: parselmouth.targeting.Custom @param value: parselmouth.targeting.Custom @return: list(parselmouth.targeting.Custom) """ with Timeout(self._network_timeout): return self.provider.create_custom_target(key, value)
def setUp(self): self.tree_builder = TreeBuilder(None, None)