diff --git a/setup.py b/setup.py index cb57be1..fae11f1 100644 --- a/setup.py +++ b/setup.py @@ -26,5 +26,8 @@ license='Apache', author='Timu Eren', author_email='selamtux@gmail.com', - description="Easy way to generate vast (3.0) xml" + description="Easy way to generate vast (3.0) xml", + install_requires=[ + 'lxml' + ] ) diff --git a/vast/ad.py b/vast/ad.py index e16c3ff..eac2d25 100644 --- a/vast/ad.py +++ b/vast/ad.py @@ -15,11 +15,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -from creative import Creative +from .creative import Creative -REQUIRED_INLINE = ['AdSystem', 'AdTitle'] -REQUIRED_WRAPPER = ['AdSystem', 'VASTAdTagURI'] +REQUIRED_INLINE = ['id', 'ad_system', 'ad_title'] +REQUIRED_WRAPPER = ['id', 'ad_system', 'vast_ad_tag_uri'] def validateSettings(settings, requireds): @@ -43,26 +43,27 @@ def __init__(self, settings={}): self.surveys = [] self.impressions = [] self.creatives = [] + self.extensions = [] if settings["structure"].lower() == 'wrapper': validateWrapperSettings(settings) - self.VASTAdTagURI = settings["VASTAdTagURI"] + self.vast_ad_tag_uri = settings["vast_ad_tag_uri"] + self.ad_title = settings.get("ad_title", None) else: validateInLineSettings(settings) + self.ad_title = settings["ad_title"] self.id = settings["id"] - self.sequence = settings.get("sequence", None) self.structure = settings["structure"] - self.AdSystem = settings["AdSystem"] - self.AdTitle = settings["AdTitle"] + self.ad_system = settings["ad_system"] # optional elements - self.Error = settings.get("Error", None) - self.Description = settings.get("Description", None) - self.Advertiser = settings.get("Advertiser", None) + self.sequence = settings.get("sequence", None) + self.error = settings.get("error", None) + self.description = settings.get("description", None) + self.avertiser = settings.get("advertiser", None) - self.Pricing = settings.get("Pricing", None) - self.Extensions = settings.get("Extensions", None) + self.pricing = settings.get("pricing", None) def attachSurvey(self, settings): survey={"url": settings.url} @@ -74,8 +75,12 @@ def attachImpression(self, settings): self.impressions.append(settings) return self - def attachCreative(self, _type, options): - creative = Creative(_type, options) + def attachCreative(self, creative_type, options): + creative = Creative(creative_type, options) self.creatives.append(creative) return creative + def attachExtension(self, extension_type, xml): + self.extensions.append({"type": extension_type, "xml": xml}) + return self + diff --git a/vast/companionAd.py b/vast/companionAd.py index 5e26973..f3a0317 100644 --- a/vast/companionAd.py +++ b/vast/companionAd.py @@ -15,7 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from trackingEvent import TrackingEvent +from .trackingEvent import TrackingEvent class CompanionAd(object): diff --git a/vast/creative.py b/vast/creative.py index 10e15f2..37b1304 100644 --- a/vast/creative.py +++ b/vast/creative.py @@ -16,17 +16,22 @@ # limitations under the License. -from companionAd import CompanionAd -from icon import Icon -from trackingEvent import TrackingEvent +from .companionAd import CompanionAd +from .icon import Icon +from .trackingEvent import TrackingEvent VALID_VIDEO_CLICKS = ['ClickThrough', 'ClickTracking', 'CustomClick'] +VALID_CREATIVE_TYPES = ['Linear', 'NonLinear', 'CompanionAds'] +REQUIRED_MEDIA_ATTRIBUTES = ['type', 'width', 'height', 'delivery'] class Creative(object): - def __init__(self, _type, settings=None): + def __init__(self, creative_type, settings=None): + if creative_type not in VALID_CREATIVE_TYPES: + raise Exception('The supplied creative type is not a valid VAST creative type.') + settings = {} if settings is None else settings - self.type = _type + self.type = creative_type self.mediaFiles = [] self.trackingEvents = [] self.videoClicks = [] @@ -34,19 +39,17 @@ def __init__(self, _type, settings=None): self.clicks = [] self.resources = [] self.icons = [] - self.AdParameters = settings.get("AdParameters", None) - self._adParameters = None + self.ad_parameters = settings.get("adParameters", None) self.attributes = {} - self.duration = settings.get("Duration", None) + self.duration = settings.get("duration", None) self.skipoffset = settings.get("skipoffset", None) self.nonLinearClickEvent = None - if _type == "Linear" and self.duration is None: + if creative_type == "Linear" and self.duration is None: raise Exception('A Duration is required for all creatives. Consider defaulting to "00:00:00"') if "id" in settings: self.attributes["id"] = settings["id"] - if "width" in settings: self.attributes["width"] = settings["width"] if "height" in settings: @@ -65,16 +68,20 @@ def __init__(self, _type, settings=None): self.attributes["apiFramework"] = settings["apiFramework"] def attachMediaFile(self, url, settings={}): + keys = settings.keys() + for required in REQUIRED_MEDIA_ATTRIBUTES: + if required not in keys: + raise Exception("MediaFile missing required settings: {required}".format(required=required)) + media_file = {"attributes": {}} media_file["url"] = url - media_file["attributes"]["type"] = settings.get("type", 'video/mp4') - media_file["attributes"]["width"] = settings.get("width",'640') - media_file["attributes"]["height"] = settings.get("height", '360') - media_file["attributes"]["delivery"]= settings.get("delivery", 'progressive') - if "id" not in settings: - raise Exception('an `id` is required for all media files') - - media_file["attributes"]["id"] = settings["id"] + media_file["attributes"]["type"] = settings.get("type") + media_file["attributes"]["width"] = settings.get("width") + media_file["attributes"]["height"] = settings.get("height") + media_file["attributes"]["delivery"]= settings.get("delivery") + + if "id" in settings: + media_file["attributes"]["id"] = settings["id"] if "bitrate" in settings: media_file["attributes"]["bitrate"] = settings["bitrate"] if "minBitrate" in settings: @@ -93,32 +100,39 @@ def attachMediaFile(self, url, settings={}): self.mediaFiles.append(media_file) return self - def attachTrackingEvent(self, _type, url, offset=None): - self.trackingEvents.append(TrackingEvent(_type, url, offset)) + def attachTrackingEvent(self, event_type, url, offset=None): + self.trackingEvents.append(TrackingEvent(event_type, url, offset)) return self - def attachVideoClick(self, _type, url, _id=''): - if _type not in VALID_VIDEO_CLICKS: + def attachVideoClick(self, click_type, url, click_id=''): + if click_type not in VALID_VIDEO_CLICKS: raise Exception('The supplied VideoClick `type` is not a valid VAST VideoClick type.') - self.videoClicks.append({"type": _type, "url": url, "id": _id}) + + self.videoClicks.append({"type": click_type, "url": url, "id": click_id}) + return self def attachClickThrough(self, url): self.clickThroughs.append(url) return self - def attachClick(self, uri, _type=None): + def attachClick(self, uri, click_type=None): if isinstance(uri, basestring): - _type = 'NonLinearClickThrough' - self.clicks = [{"type": _type, "uri": uri}] + click_type = 'NonLinearClickThrough' + + self.clicks = [{"type": click_type, "uri": uri}] + return self - def attachResource(self, _type, uri, creative_type=None): - resource = {"type": _type, "uri": uri} - if _type == 'HTMLResource': + def attachResource(self, resource_type, uri, creative_type=None): + resource = {"type": resource_type, "uri": uri} + + if resource_type == 'HTMLResource': resource["html"] = uri + if creative_type is not None: resource["creativeType"] = creative_type + self.resources.append(resource) return self @@ -128,7 +142,7 @@ def attachIcon(self, settings): return icon def adParameters(self, data, xml_encoded): - self._adParameters = {"data": data, "xmlEncoded": xml_encoded} + self.ad_parameters = {"data": data, "xmlEncoded": xml_encoded} return self def attachNonLinearClickTracking(self, url): diff --git a/vast/icon.py b/vast/icon.py index 49db50c..c391b1f 100644 --- a/vast/icon.py +++ b/vast/icon.py @@ -16,13 +16,13 @@ # limitations under the License. -REQURED_ATTRIBUTES = ["program", "width", "height", "xPosition", "yPosition"] +REQUIRED_ATTRIBUTES = ["program", "width", "height", "xPosition", "yPosition"] class Icon(object): def __init__(self, settings=dict()): keys = settings.keys() - for required in keys: + for required in REQUIRED_ATTRIBUTES: if required not in keys: raise Exception("Missing required attribute '{attr}'".format(attr=required)) @@ -33,15 +33,18 @@ def __init__(self, settings=dict()): self.click = None self.view = None - def setResource(self, _type, uri, creativeType=None): - if _type not in ('StaticResource', "IFrameResource", "HTMLResource"): + def setResource(self, resource_type, uri, creativeType=None): + if resource_type not in ('StaticResource', "IFrameResource", "HTMLResource"): raise Exception("Invalid resource type") - resource = {"type": _type, "uri": uri} - if _type == 'HTMLResource': + resource = {"type": resource_type, "uri": uri} + + if resource_type == 'HTMLResource': resource["html"] = uri + if creativeType: resource["creativeType"] = creativeType + self.resource = resource def setClickThrough(self, uri): diff --git a/vast/vast.py b/vast/vast.py old mode 100644 new mode 100755 index 82f1a06..8bcbfa4 --- a/vast/vast.py +++ b/vast/vast.py @@ -15,171 +15,231 @@ # See the License for the specific language governing permissions and # limitations under the License. -from ad import Ad -from xmlbuilder import XMLBuilder +from .ad import Ad +from xml.etree import ElementTree as et +from lxml import etree class VAST(object): def __init__(self, settings={}): self.ads = [] self.version = settings.get("version", "3.0") - self.VASTErrorURI = settings.get("VASTErrorURI", None) + self.vast_error_uri = settings.get("vast_error_uri", None) - def attachAd(self, settings): + def attachAd(self, ad): + self.ads.append(ad) + return ad + + def createAd(self, settings): ad = Ad(settings) self.ads.append(ad) return ad def cdata(self, param): - return param - #return """""".format(param=param) - # + return """""".format(param=param) - def add_creatives(self, response, ad, track): + def add_creatives(self, elem, ad): linearCreatives = [c for c in ad.creatives if c.type == "Linear"] nonLinearCreatives = [c for c in ad.creatives if c.type == "NonLinear"] companionAdCreatives = [c for c in ad.creatives if c.type == "CompanionAd"] - with response.Creatives: - for creative in linearCreatives: - creativeOpts = {} + + creativesElem = etree.SubElement(elem, "Creatives") + + for creative in linearCreatives: + creativeElem = etree.SubElement(creativesElem, "Creative") + + linearOptions = {} + if creative.skipoffset: + linearOptions['skipoffset'] = str(creative.skipoffset) + + linearElem = etree.SubElement(creativeElem, "Linear", linearOptions) + + durationElem = etree.SubElement(linearElem, "Duration") + durationElem.text = str(creative.duration) + + if creative.ad_parameters: + adParametersOptions = {} + if creative.ad_parameters.xmlEncoded: + adParametersOptions['xmlEncoded'] = str(creative.ad_parameters.xmlEncoded) + + adParametersElem = etree.SubElement(linearElem, "AdParameters", adParametersOptions) + adParametersElem.text = etree.CDATA(creative.AdParameters.data) + + if creative.trackingEvents: + trackingEventsElem = etree.SubElement(linearElem, "TrackingEvents") + + for event in creative.trackingEvents: + trackingEventOptions = {"event": str(event.event)} + + if event.offset: + trackingEventOptions["offset"] = str(event.offset) + + trackingEventElem = etree.SubElement(trackingEventsElem, "Tracking", trackingEventOptions) + trackingEventElem.text = etree.CDATA(event.url) + + if creative.videoClicks: + videoClicksElem = etree.SubElement(linearElem, "VideoClicks") + + for click in creative.videoClicks: + if ad.structure.lower() != 'wrapper' or click['type'] != 'ClickThrough': + clickOptions = {}; + + if click['id']: + clickOptions['id'] = str(click['id']); + + clickElem = etree.SubElement(videoClicksElem, click['type'], clickOptions) + clickElem.text = etree.CDATA(click['url']) + + if creative.mediaFiles and ad.structure.lower() != 'wrapper': + mediaFilesElem = etree.SubElement(linearElem, "MediaFiles") + + for media in creative.mediaFiles: + mediaFileElem = etree.SubElement(mediaFilesElem, "MediaFile", media["attributes"]) + mediaFileElem.text = etree.CDATA(media["url"]) + + if len(creative.icons) > 0: + iconsElem = etree.SubElement(linearElem, "Icons") + for icon in creative.icons: + iconElem = etree.SubElement(iconsElem, "Icon", icon.attributes) + + with response.Icon(**icon.attributes): + attributes = {} + if "creativeType" in icon.resource: + attributes["creativeType"] = icon.resource["creativeType"] + attr = getattr(response, icon.resource["type"]) + attr(icon.resource["uri"], **attributes) + if icon.click or icon.clickThrough: + with response.IconClicks: + if icon.clickThrough: + response.IconClickThrough(icon.clickThrough) + if icon.click: + response.IconClickTraking(icon.click) + if icon.view: + response.IconViewTracking(icon.view) + + ''' + if len(nonLinearCreatives) > 0: + for creative in nonLinearCreatives: with response.Creative: - if creative.skipoffset: - creativeOpts["skipoffset"] = creative.skipoffset - with response.Linear(**creativeOpts): - if len(creative.icons) > 0: - with response.Icons: - for icon in creative.icons: - with response.Icon(**icon.attributes): - attributes = {} - if "creativeType" in icon.resource: - attributes["creativeType"] = icon.resource["creativeType"] - attr = getattr(response, icon.resource["type"]) - attr(icon.resource["uri"], **attributes) - if icon.click or icon.clickThrough: - with response.IconClicks: - if icon.clickThrough: - response.IconClickThrough(icon.clickThrough) - if icon.click: - response.IconClickTraking(icon.click) - if icon.view: - response.IconViewTracking(icon.view) - response.Duration(creative.duration) - with response.TrackingEvents: - for event in creative.trackingEvents: - if track: - attrs = {"event": event.event} - if event.offset: - attrs["offset"] = event.offset - response.Tracking(event.url, **attrs) - if creative.AdParameters: - response.AddParameters(creative.AdParameters) - with response.VideoClicks: - for click in creative.videoClicks: - attr = getattr(response, click["type"]) - attr(click["url"], **{"id": click.get("id", "")}) - - with response.MediaFiles: - for media in creative.mediaFiles: - response.MediaFile(media["url"], **media["attributes"]) - - if len(nonLinearCreatives) > 0: - for creative in nonLinearCreatives: - with response.Creative: - with response.NonLinearAds: - with response.NonLinear(**creative.attributes): - for resource in creative.resources: - attrs = {} - if "creativeType" in resource: - attrs["creativeType"] = resource["creativeType"] - element = getattr(response, resource["type"]) - element(resource["uri"], **attrs) - - for click in creative.clicks: - element = getattr(response, click["type"]) - element(click["uri"]) - - if creative.AdParameters: - response.AdParameters(creative.AdParameters["data"], **{ - "xmlEncoded": creative.AdParameters["xmlEncoded"] - }) - if creative.nonLinearClickEvent: - response.NonLinearClickTracking(creative.nonLinearClickEvent) - - if len(companionAdCreatives) > 0: - with response.CompanionAds: - for creative in companionAdCreatives: - with response.Companion(**creative.attributes): + with response.NonLinearAds: + with response.NonLinear(**creative.attributes): for resource in creative.resources: attrs = {} - element = getattr(response, resource["type"]) if "creativeType" in resource: attrs["creativeType"] = resource["creativeType"] + element = getattr(response, resource["type"]) element(resource["uri"], **attrs) - if "adParameters" in resource: - response.AdParameters(resource["adParameters"]["data"], **{ - "xmlEncoded": resource["adParameters"]["xmlEncoded"] - }) - with response.TrakingEvents: - for event in creative.trackingEvents: - if track: - attrs = {"event": event.event} - if event.offset: - attrs["offset"] = event.offset - response.Tracking(event.url, **attrs) - - for click in creative.clickThroughs: - response.CompanionClickThrough(click) + for click in creative.clicks: + element = getattr(response, click["type"]) + element(click["uri"]) + + if creative.AdParameters: + response.AdParameters(creative.AdParameters["data"], **{ + "xmlEncoded": creative.AdParameters["xmlEncoded"] + }) if creative.nonLinearClickEvent: - response.CompanionClickTracking(creative.nonLinearClickEvent) + response.NonLinearClickTracking(creative.nonLinearClickEvent) + + if len(companionAdCreatives) > 0: + with response.CompanionAds: + for creative in companionAdCreatives: + with response.Companion(**creative.attributes): + for resource in creative.resources: + attrs = {} + element = getattr(response, resource["type"]) + if "creativeType" in resource: + attrs["creativeType"] = resource["creativeType"] + element(resource["uri"], **attrs) + if "adParameters" in resource: + response.AdParameters(resource["adParameters"]["data"], **{ + "xmlEncoded": resource["adParameters"]["xmlEncoded"] + }) + with response.TrakingEvents: + for event in creative.trackingEvents: + if track: + attrs = {"event": event.event} + if event.offset: + attrs["offset"] = event.offset + response.Tracking(event.url, **attrs) + + for click in creative.clickThroughs: + response.CompanionClickThrough(click) + + if creative.nonLinearClickEvent: + response.CompanionClickTracking(creative.nonLinearClickEvent) + ''' + + def formatXmlResponse(self, response): + response = etree.tostring(response, pretty_print=True, encoding="UTF-8") + return response.decode("utf-8") def xml(self, options={}): - track = True if options.get("track", True) else options.get("track") - response = XMLBuilder('VAST', version=self.version) - if len(self.ads) == 0 and self.VASTErrorURI: - response.Error(self.cdata(self.VASTErrorURI)) - return response + root = etree.Element('VAST') + root.set("version", str(self.version)) + + if len(self.ads) == 0 and self.vast_error_uri: + etree.SubElement(root, "Error", etree.CDATA(self.vast_error_uri)) + return self.formatXmlResponse(root) + for ad in self.ads: - adOptions = {"id": ad.id} + adOptions = { 'id': str(ad.id) } + if ad.sequence: - adOptions["sequence"] = str(ad.sequence) - - with response.Ad(**adOptions): - if ad.structure.lower() == 'wrapper': - with response.Wrapper: - response.AdSystem(ad.AdSystem["name"], **{"version": ad.AdSystem["version"]}) - response.VASTAdTagURI(self.cdata(ad.VASTAdTagURI)) - if ad.Error: - response.Error(self.cdata(ad.Error)) - for impression in ad.impressions: - if track: - response.Impression(self.cdata(impression["url"])) - self.add_creatives(response, ad, track) - else: - with response.InLine: - response.AdSystem(ad.AdSystem["name"], **{"version": ad.AdSystem["version"]}) - response.AdTitle(self.cdata(ad.AdTitle)) - response.Description(self.cdata(ad.Description or '')) - - with response.Survey: - for survey in ad.surveys: - attributes = {} - if survey.type: - attributes["type"] = survey.type - response.Survey(self.cdata(survey.url), **attributes) - - if ad.Error: - response.Error(self.cdata(ad.Error)) - - for impression in ad.impressions: - if track: - response.Impression(self.cdata(impression["url"])) - - self.add_creatives(response, ad, track) - - if ad.Extensions: - for extension in ad.Extensions: - response.Extension(extension) - return response + adOptions['sequence'] = str(ad.sequence) + + adElem = etree.SubElement(root, "Ad", adOptions) + + if ad.structure.lower() == 'wrapper': + wrapperElem = etree.SubElement(adElem, "Wrapper") + + adSystemElem = etree.SubElement(wrapperElem, "AdSystem") + adSystemElem.text = str(ad.ad_system) + + vastAdTagElem = etree.SubElement(wrapperElem, "VASTAdTagURI") + vastAdTagElem.text = etree.CDATA(ad.vast_ad_tag_uri) + + if ad.error: + adErrorElem = etree.SubElement(wrapperElem, "Error") + adErrorElem.text = etree.CDATA(ad.error) + + for impression in ad.impressions: + impressionElem = etree.SubElement(wrapperElem, "Impression") + impressionElem.text = etree.CDATA(impression["url"]) + + self.add_creatives(wrapperElem, ad) + else: + inlineElem = etree.SubElement(adElem, "InLine") + + adSystemElem = etree.SubElement(inlineElem, "AdSystem") + adSystemElem.text = str(ad.ad_system) + + adTitleElem = etree.SubElement(inlineElem, "AdTitle") + adTitleElem.text = etree.CDATA(ad.ad_title) + + if ad.description: + adDescriptionElem = etree.SubElement(inlineElem, "Description") + adDescriptionElem.text = etree.CDATA(ad.description) + + if ad.error: + adErrorElem = etree.SubElement(inlineElem, "Error") + adErrorElem.text = etree.CDATA(ad.error) + + for impression in ad.impressions: + impressionElem = etree.SubElement(inlineElem, "Impression") + impressionElem.text = etree.CDATA(impression["url"]) + + self.add_creatives(inlineElem, ad) + + if ad.extensions: + extensionsElem = etree.SubElement(inlineElem, "Extensions") + + for extension in ad.extensions: + extensionElem = etree.SubElement(inlineElem, "Extension") + + if (extension['type']): + extensionElem.set("type", extension['type']); + + extensionElem.append(etree.fromstring(extension['xml'])) + + return self.formatXmlResponse(root)