def main(): with open('thebugle.json') as f: episodes = json.load(f) p = Podcast( name="TimesOnLine Bugle Archive", description="Old Bugle episodes, podcast feed", website="https://www.thebuglepodcast.com/", explicit=False, ) for episode in episodes: ep = p.add_episode( Episode(title=f"{episode['id']}: {episode['title']}")) ep.media = Media.create_from_server_response( f"{MEDIA_BASE_URL}/{episode['file']}") ep.media.fetch_duration() date = episode['date'].split('-') ep.publication_date = datetime(int(date[0]), int(date[1]), int(date[2]), 0, 0, 0, tzinfo=pytz.utc) print(p.rss_str())
def main(): """Create an example podcast and print it or save it to a file.""" # There must be exactly one argument, and it is must end with rss if len(sys.argv) != 2 or not ( sys.argv[1].endswith('rss')): # Invalid usage, print help message # print_enc is just a custom function which functions like print, # except it deals with byte arrays properly. print_enc ('Usage: %s ( <file>.rss | rss )' % \ 'python -m podgen') print_enc ('') print_enc (' rss -- Generate RSS test output and print it to stdout.') print_enc (' <file>.rss -- Generate RSS test teed and write it to file.rss.') print_enc ('') exit() # Remember what type of feed the user wants arg = sys.argv[1] from podgen import Podcast, Person, Media, Category, htmlencode # Initialize the feed p = Podcast() p.name = 'Testfeed' p.authors.append(Person("Lars Kiesow", "*****@*****.**")) p.website = 'http://example.com' p.copyright = 'cc-by' p.description = 'This is a cool feed!' p.language = 'de' p.feed_url = 'http://example.com/feeds/myfeed.rss' p.category = Category('Technology', 'Podcasting') p.explicit = False p.complete = False p.new_feed_url = 'http://example.com/new-feed.rss' p.owner = Person('John Doe', '*****@*****.**') p.xslt = "http://example.com/stylesheet.xsl" e1 = p.add_episode() e1.id = 'http://lernfunk.de/_MEDIAID_123#1' e1.title = 'First Element' e1.summary = htmlencode('''Lorem ipsum dolor sit amet, consectetur adipiscing elit. Tamen aberramus a proposito, et, ne longius, prorsus, inquam, Piso, si ista mala sunt, placet. Aut etiam, ut vestitum, sic sententiam habeas aliam domesticam, aliam forensem, ut in fronte ostentatio sit, intus veritas occultetur? Cum id fugiunt, re eadem defendunt, quae Peripatetici, verba <3.''') e1.link = 'http://example.com' e1.authors = [Person('Lars Kiesow', '*****@*****.**')] e1.publication_date = datetime.datetime(2014, 5, 17, 13, 37, 10, tzinfo=pytz.utc) e1.media = Media("http://example.com/episodes/loremipsum.mp3", 454599964, duration= datetime.timedelta(hours=1, minutes=32, seconds=19)) # Should we just print out, or write to file? if arg == 'rss': # Print print_enc(p.rss_str()) elif arg.endswith('rss'): # Write to file p.rss_file(arg, minimize=True)
def generate_podcast_xml(podcasts): podcast = Podcast(name=config.PODCAST_NAME, description=config.PODCAST_DESCRIPTION, website=config.PODCAST_WEBSITE, explicit=config.PODCAST_CONTAINS_EXPLICIT_CONTENT, withhold_from_itunes=True) podcast.episodes = podcasts return podcast.rss_str()
def main(event, context): dynamodb = boto3.resource('dynamodb', region_name='sa-east-1') table = dynamodb.Table('semservidor-dev') podcasts = table.scan() author = Person("Evandro Pires da Silva", "*****@*****.**") p = Podcast( name="Sem Servidor", description= "Podcast dedicado a arquitetura serverless, com conteúdo de qualidade em português.", website="https://semservidor.com.br", explicit=False, copyright="2020 Evandro Pires da Silva", language="pr-BR", authors=[author], feed_url= "https://3tz8r90j0d.execute-api.sa-east-1.amazonaws.com/dev/podcasts/rss", category=Category("Music", "Music History"), owner=author, image="http://d30gvsirhz3ono.cloudfront.net/logo_semservidor_teste.jpg", web_master=Person(None, "*****@*****.**")) items = podcasts['Items'] for item in items: base_url = "http://d30gvsirhz3ono.cloudfront.net/" file_path = base_url + item['info']['arquivo']['nome'] p.episodes += [ Episode(title=item['info']['episodio'], media=Media(file_path, int(item['info']['arquivo']['tamanho'])), summary=item['info']['descricao'], position=int(item['id'])) ] p.apply_episode_order() rss = p.rss_str() response = { "statusCode": 200, "headers": { "content-type": "application/xml" }, "body": rss } return response
def scrape_by_program(program, web_session=requests_html.HTMLSession(), params=params): podcast = Podcast() podcast.explicit = False podcast.website = params[PARAMS_BASEURL].format(program=program) if program == 'morning-edition': podcast.name = "NPR Morning Edition" podcast.description = \ """Every weekday for over three decades, Morning Edition has taken listeners around the country and the world with two hours of multi-faceted stories and commentaries that inform, challenge and occasionally amuse. Morning Edition is the most listened-to news radio program in the country.""" podcast.image = 'https://media.npr.org/assets/img/2018/08/06/npr_me_podcasttile_sq-4036eb96471eeed96c37dfba404bb48ea798e78c-s200-c85.jpg' elif program == 'all-things-considered': podcast.name = "NPR All Things Considered" podcast.description = \ """NPR's afternoon news show""" podcast.image = 'https://media.npr.org/assets/img/2018/08/06/npr_atc_podcasttile_sq-bcc33a301405d37aa6bdcc090f43d29264915f4a-s200-c85.jpg' elif program == 'weekend-edition-saturday': podcast.name = "NPR Weekend Edition Saturday" podcast.description = \ """NPR morning news on Saturday""" podcast.image = 'https://media.npr.org/assets/img/2019/02/26/we_otherentitiestemplatesat_sq-cbde87a2fa31b01047441e6f34d2769b0287bcd4-s200-c85.png' elif program == 'weekend-edition-sunday': podcast.name = "NPR Weekend Edition Sunday" podcast.description = \ """NPR morning news show on Sunday""" podcast.image = 'https://media.npr.org/assets/img/2019/02/26/we_otherentitiestemplatesun_sq-4a03b35e7e5adfa446aec374523a578d54dc9bf5-s200-c85.png' else: raise WebFormatException(f"program { program } not found") scrape(web_session, params, program, podcast) rssfeed = podcast.rss_str(minimize=False) #log.debug(f"\n\nfeed { rssfeed }") return rssfeed
def scrape_morning_edition( web_session=requests_html.HTMLSession(), params=params): podcast = Podcast() podcast.name = "NPR Morning Edition" podcast.description = \ """Every weekday for over three decades, Morning Edition has taken listeners around the country and the world with two hours of multi-faceted stories and commentaries that inform, challenge and occasionally amuse. Morning Edition is the most listened-to news radio program in the country.""" podcast.website = "https://www.npr.org/programs/morning-edition" podcast.explicit = False scrape(web_session, params, 'morning-edition', podcast) rssfeed = podcast.rss_str(minimize=False) #log.debug(f"\n\nfeed { rssfeed }") return rssfeed
def generate_podcast(self, feed_name: str) -> str: """ Create podcast XML based on the files found in podcastDir. Taken from https://podgen.readthedocs.io/en/latest/usage_guide/podcasts.html :param self: PodcastService class :param feed_name: name of the feed and the sub-directory for files :return: string of the podcast """ # Initialize the feed p = Podcast() # Required fields p.name = f'{feed_name} Archive' p.description = 'Stuff to listen to later' p.website = self.base_url p.complete = False # Optional p.language = 'en-US' p.feed_url = f'{p.website}/feeds/{feed_name}/rss' p.explicit = False p.authors.append(Person("Anthology")) # for filepath in glob.iglob(f'{self.search_dir}/{feed_name}/*.mp3'): for path in Path(f'{self.search_dir}/{feed_name}').glob('**/*.mp3'): filepath = str(path) episode = p.add_episode() # Attempt to load saved metadata metadata_file_name = filepath.replace('.mp3', '.json') try: with open(metadata_file_name) as metadata_file: metadata = json.load(metadata_file) except FileNotFoundError: metadata = {} except JSONDecodeError: metadata = {} self.logger.error(f'Failed to read {metadata_file_name}') # Build the episode based on either the saved metadata or the file details episode.title = metadata.get( 'title', filepath.split('/')[-1].rstrip('.mp3')) episode.summary = metadata.get('summary', htmlencode('Some Summary')) if 'link' in metadata: episode.link = metadata.get('link') if 'authors' in metadata: episode.authors = [ Person(author) for author in metadata.get('authors') ] episode.publication_date = \ isoparse(metadata.get('publication_date')) if 'publication_date' in metadata \ else datetime.fromtimestamp(os.path.getmtime(filepath), tz=pytz.utc) episode.media = Media( f'{p.website}/{filepath.lstrip(self.search_dir)}'.replace( ' ', '+'), os.path.getsize(filepath)) episode.media.populate_duration_from(filepath) if "image" in metadata: episode.image = metadata.get('image') else: for ext in ['.jpg', '.png']: image_file_name = filepath.replace('.mp3', ext) if os.path.isfile(image_file_name): episode.image = f'{p.website}/{image_file_name.lstrip(self.search_dir)}'.replace( ' ', '+') break # Save the metadata for future editing if not os.path.exists(metadata_file_name): metadata = { 'title': episode.title, 'summary': episode.summary, 'publication_date': episode.publication_date, 'authors': episode.authors } with open(metadata_file_name, 'w') as outFile: json.dump(metadata, outFile, indent=2, default=str) return p.rss_str()
class TestPodcast(unittest.TestCase): def setUp(self): self.existing_locale = locale.setlocale(locale.LC_ALL, None) locale.setlocale(locale.LC_ALL, 'C') fg = Podcast() self.nsContent = "http://purl.org/rss/1.0/modules/content/" self.nsDc = "http://purl.org/dc/elements/1.1/" self.nsItunes = "http://www.itunes.com/dtds/podcast-1.0.dtd" self.feed_url = "http://example.com/feeds/myfeed.rss" self.name = 'Some Testfeed' # Use character not in ASCII to catch encoding errors self.author = Person('Jon Døll', '*****@*****.**') self.website = 'http://example.com' self.description = 'This is a cool feed!' self.subtitle = 'Coolest of all' self.language = 'en' self.cloudDomain = 'example.com' self.cloudPort = '4711' self.cloudPath = '/ws/example' self.cloudRegisterProcedure = 'registerProcedure' self.cloudProtocol = 'SOAP 1.1' self.pubsubhubbub = "http://pubsubhubbub.example.com/" self.contributor = { 'name': "Contributor Name", 'email': 'Contributor email' } self.copyright = "The copyright notice" self.docs = 'http://www.rssboard.org/rss-specification' self.skip_days = set(['Tuesday']) self.skip_hours = set([23]) self.explicit = False self.programname = podgen.version.name self.web_master = Person(email='*****@*****.**') self.image = "http://example.com/static/podcast.png" self.owner = self.author self.complete = True self.new_feed_url = "https://example.com/feeds/myfeed2.rss" self.xslt = "http://example.com/feed/stylesheet.xsl" fg.name = self.name fg.website = self.website fg.description = self.description fg.subtitle = self.subtitle fg.language = self.language fg.cloud = (self.cloudDomain, self.cloudPort, self.cloudPath, self.cloudRegisterProcedure, self.cloudProtocol) fg.pubsubhubbub = self.pubsubhubbub fg.copyright = self.copyright fg.authors.append(self.author) fg.skip_days = self.skip_days fg.skip_hours = self.skip_hours fg.web_master = self.web_master fg.feed_url = self.feed_url fg.explicit = self.explicit fg.image = self.image fg.owner = self.owner fg.complete = self.complete fg.new_feed_url = self.new_feed_url fg.xslt = self.xslt self.fg = fg warnings.simplefilter("always") def noop(*args, **kwargs): pass warnings.showwarning = noop def tearDown(self): locale.setlocale(locale.LC_ALL, self.existing_locale) def test_constructor(self): # Overwrite fg from setup self.fg = Podcast( name=self.name, website=self.website, description=self.description, subtitle=self.subtitle, language=self.language, cloud=(self.cloudDomain, self.cloudPort, self.cloudPath, self.cloudRegisterProcedure, self.cloudProtocol), pubsubhubbub=self.pubsubhubbub, copyright=self.copyright, authors=[self.author], skip_days=self.skip_days, skip_hours=self.skip_hours, web_master=self.web_master, feed_url=self.feed_url, explicit=self.explicit, image=self.image, owner=self.owner, complete=self.complete, new_feed_url=self.new_feed_url, xslt=self.xslt, ) # Test that the fields are actually set self.test_baseFeed() def test_constructorUnknownAttributes(self): self.assertRaises(TypeError, Podcast, naem="Oh, looks like a typo") self.assertRaises(TypeError, Podcast, "Haha, No Keyword") def test_baseFeed(self): fg = self.fg assert fg.name == self.name assert fg.authors[0] == self.author assert fg.web_master == self.web_master assert fg.website == self.website assert fg.description == self.description assert fg.subtitle == self.subtitle assert fg.language == self.language assert fg.feed_url == self.feed_url assert fg.image == self.image assert fg.owner == self.owner assert fg.complete == self.complete assert fg.pubsubhubbub == self.pubsubhubbub assert fg.cloud == (self.cloudDomain, self.cloudPort, self.cloudPath, self.cloudRegisterProcedure, self.cloudProtocol) assert fg.copyright == self.copyright assert fg.new_feed_url == self.new_feed_url assert fg.skip_days == self.skip_days assert fg.skip_hours == self.skip_hours assert fg.xslt == self.xslt def test_rssFeedFile(self): fg = self.fg rssString = self.getRssFeedFileContents(fg, xml_declaration=False)\ .replace('\n', '') self.checkRssString(rssString) def getRssFeedFileContents(self, fg, **kwargs): # Keep track of our temporary file and its filename filename = None file = None encoding = 'UTF-8' try: # Get our temporary file name file = tempfile.NamedTemporaryFile(delete=False) filename = file.name # Close the file; we will just use its name file.close() # Write the RSS to the file (overwriting it) fg.rss_file(filename=filename, encoding=encoding, **kwargs) # Read the resulting RSS with open(filename, "r", encoding=encoding) as myfile: rssString = myfile.read() finally: # We don't need the file any longer, so delete it if filename: os.unlink(filename) elif file: # Ops, we were interrupted between the first and second stmt filename = file.name file.close() os.unlink(filename) else: # We were interrupted between entering the try-block and # getting the temporary file. Not much we can do. pass return rssString def test_rssFeedString(self): fg = self.fg rssString = fg.rss_str(xml_declaration=False) self.checkRssString(rssString) def test_rssStringAndFileAreEqual(self): rss_string = self.fg.rss_str() rss_file = self.getRssFeedFileContents(self.fg) self.assertEqual(rss_string, rss_file) def checkRssString(self, rssString): feed = etree.fromstring(rssString) nsRss = self.nsContent nsAtom = "http://www.w3.org/2005/Atom" channel = feed.find("channel") assert channel != None assert channel.find("title").text == self.name assert channel.find("description").text == self.description assert channel.find("{%s}subtitle" % self.nsItunes).text == \ self.subtitle assert channel.find("link").text == self.website assert channel.find("lastBuildDate").text != None assert channel.find("language").text == self.language assert channel.find( "docs").text == "http://www.rssboard.org/rss-specification" assert self.programname in channel.find("generator").text assert channel.find("cloud").get('domain') == self.cloudDomain assert channel.find("cloud").get('port') == self.cloudPort assert channel.find("cloud").get('path') == self.cloudPath assert channel.find("cloud").get( 'registerProcedure') == self.cloudRegisterProcedure assert channel.find("cloud").get('protocol') == self.cloudProtocol assert channel.find("copyright").text == self.copyright assert channel.find("docs").text == self.docs assert self.author.email in channel.find("managingEditor").text assert channel.find("skipDays").find("day").text in self.skip_days assert int( channel.find("skipHours").find("hour").text) in self.skip_hours assert self.web_master.email in channel.find("webMaster").text links = channel.findall("{%s}link" % nsAtom) selflinks = [link for link in links if link.get('rel') == 'self'] hublinks = [link for link in links if link.get('rel') == 'hub'] assert selflinks, "No <atom:link rel='self'> element found" selflink = selflinks[0] assert selflink.get('href') == self.feed_url assert selflink.get('type') == 'application/rss+xml' assert hublinks, "No <atom:link rel='hub'> element found" hublink = hublinks[0] assert hublink.get('href') == self.pubsubhubbub assert hublink.get('type') is None assert channel.find("{%s}image" % self.nsItunes).get('href') == \ self.image owner = channel.find("{%s}owner" % self.nsItunes) assert owner.find("{%s}name" % self.nsItunes).text == self.owner.name assert owner.find("{%s}email" % self.nsItunes).text == self.owner.email assert channel.find("{%s}complete" % self.nsItunes).text.lower() == \ "yes" assert channel.find("{%s}new-feed-url" % self.nsItunes).text == \ self.new_feed_url def test_feedUrlValidation(self): self.assertRaises(ValueError, setattr, self.fg, "feed_url", "example.com/feed.rss") def test_generator(self): software_name = "My Awesome Software" software_version = (1, 0) software_url = "http://example.com/awesomesoft/" # Using set_generator, text includes python-podgen self.fg.set_generator(software_name) rss = self.fg._create_rss() generator = rss.find("channel").find("generator").text assert software_name in generator assert self.programname in generator # Using set_generator, text excludes python-podgen self.fg.set_generator(software_name, exclude_podgen=True) generator = self.fg._create_rss().find("channel").find( "generator").text assert software_name in generator assert self.programname not in generator # Using set_generator, text includes name, version and url self.fg.set_generator(software_name, software_version, software_url) generator = self.fg._create_rss().find("channel").find( "generator").text assert software_name in generator assert str(software_version[0]) in generator assert str(software_version[1]) in generator assert software_url in generator # Using generator directly, text excludes python-podgen self.fg.generator = software_name generator = self.fg._create_rss().find("channel").find( "generator").text assert software_name in generator assert self.programname not in generator def test_str(self): assert str(self.fg) == self.fg.rss_str(minimize=False, encoding="UTF-8", xml_declaration=True) def test_updated(self): date = datetime.datetime(2016, 1, 1, 0, 10, tzinfo=dateutil.tz.tzutc()) def getLastBuildDateElement(fg): return fg._create_rss().find("channel").find("lastBuildDate") # Test that it has a default assert getLastBuildDateElement(self.fg) is not None # Test that it respects my custom value self.fg.last_updated = date lastBuildDate = getLastBuildDateElement(self.fg) assert lastBuildDate is not None assert dateutil.parser.parse(lastBuildDate.text) == date # Test that it is left out when set to False self.fg.last_updated = False lastBuildDate = getLastBuildDateElement(self.fg) assert lastBuildDate is None def test_AuthorEmail(self): # Just email - so use managingEditor, not dc:creator or itunes:author # This is per the RSS best practices, see the section about dc:creator self.fg.authors = [Person(None, "*****@*****.**")] channel = self.fg._create_rss().find("channel") # managingEditor uses email? assert channel.find("managingEditor").text == self.fg.authors[0].email # No dc:creator? assert channel.find("{%s}creator" % self.nsDc) is None # No itunes:author? assert channel.find("{%s}author" % self.nsItunes) is None def test_AuthorName(self): # Just name - use dc:creator and itunes:author, not managingEditor self.fg.authors = [Person("Just a. Name")] channel = self.fg._create_rss().find("channel") # No managingEditor? assert channel.find("managingEditor") is None # dc:creator equals name? assert channel.find("{%s}creator" % self.nsDc).text == \ self.fg.authors[0].name # itunes:author equals name? assert channel.find("{%s}author" % self.nsItunes).text == \ self.fg.authors[0].name def test_AuthorNameAndEmail(self): # Both name and email - use managingEditor and itunes:author, # not dc:creator self.fg.authors = [Person("Both a name", "*****@*****.**")] channel = self.fg._create_rss().find("channel") # Does managingEditor follow the pattern "email (name)"? self.assertEqual( self.fg.authors[0].email + " (" + self.fg.authors[0].name + ")", channel.find("managingEditor").text) # No dc:creator? assert channel.find("{%s}creator" % self.nsDc) is None # itunes:author uses name only? assert channel.find("{%s}author" % self.nsItunes).text == \ self.fg.authors[0].name def test_multipleAuthors(self): # Multiple authors - use itunes:author and dc:creator, not # managingEditor. person1 = Person("Multiple", "*****@*****.**") person2 = Person("Are", "*****@*****.**") self.fg.authors = [person1, person2] channel = self.fg._create_rss().find("channel") # Test dc:creator author_elements = \ channel.findall("{%s}creator" % self.nsDc) author_texts = [e.text for e in author_elements] assert len(author_texts) == 2 assert person1.name in author_texts[0] assert person1.email in author_texts[0] assert person2.name in author_texts[1] assert person2.email in author_texts[1] # Test itunes:author itunes_author = channel.find("{%s}author" % self.nsItunes) assert itunes_author is not None itunes_author_text = itunes_author.text assert person1.name in itunes_author_text assert person1.email not in itunes_author_text assert person2.name in itunes_author_text assert person2.email not in itunes_author_text # Test that managingEditor is not used assert channel.find("managingEditor") is None def test_authorsInvalidValue(self): self.assertRaises(TypeError, self.do_authorsInvalidValue) def do_authorsInvalidValue(self): self.fg.authors = Person("Opsie", "*****@*****.**") def test_webMaster(self): self.fg.web_master = Person(None, "*****@*****.**") channel = self.fg._create_rss().find("channel") assert channel.find("webMaster").text == self.fg.web_master.email self.assertRaises(ValueError, setattr, self.fg, "web_master", Person("Mr. No Email Address")) self.fg.web_master = Person("Both a name", "*****@*****.**") channel = self.fg._create_rss().find("channel") # Does webMaster follow the pattern "email (name)"? self.assertEqual( self.fg.web_master.email + " (" + self.fg.web_master.name + ")", channel.find("webMaster").text) def test_categoryWithoutSubcategory(self): c = Category("Arts") self.fg.category = c channel = self.fg._create_rss().find("channel") itunes_category = channel.find("{%s}category" % self.nsItunes) assert itunes_category is not None self.assertEqual(itunes_category.get("text"), c.category) assert itunes_category.find("{%s}category" % self.nsItunes) is None def test_categoryWithSubcategory(self): c = Category("Arts", "Food") self.fg.category = c channel = self.fg._create_rss().find("channel") itunes_category = channel.find("{%s}category" % self.nsItunes) assert itunes_category is not None itunes_subcategory = itunes_category\ .find("{%s}category" % self.nsItunes) assert itunes_subcategory is not None self.assertEqual(itunes_subcategory.get("text"), c.subcategory) def test_categoryChecks(self): c = ("Arts", "Food") self.assertRaises(TypeError, setattr, self.fg, "category", c) def test_explicitIsExplicit(self): self.fg.explicit = True channel = self.fg._create_rss().find("channel") itunes_explicit = channel.find("{%s}explicit" % self.nsItunes) assert itunes_explicit is not None assert itunes_explicit.text.lower() in ("yes", "explicit", "true"),\ "itunes:explicit was %s, expected yes, explicit or true" \ % itunes_explicit.text def test_explicitIsClean(self): self.fg.explicit = False channel = self.fg._create_rss().find("channel") itunes_explicit = channel.find("{%s}explicit" % self.nsItunes) assert itunes_explicit is not None assert itunes_explicit.text.lower() in ("no", "clean", "false"),\ "itunes:explicit was %s, expected no, clean or false" \ % itunes_explicit.text def test_mandatoryValues(self): # Try to create a Podcast once for each mandatory property. # On each iteration, exactly one of the properties is not set. # Therefore, an exception should be thrown on each iteration. mandatory_properties = set([ "description", "title", "link", "explicit", ]) for test_property in mandatory_properties: fg = Podcast() if test_property != "description": fg.description = self.description if test_property != "title": fg.name = self.name if test_property != "link": fg.website = self.website if test_property != "explicit": fg.explicit = self.explicit try: self.assertRaises(ValueError, fg._create_rss) except AssertionError as e: raise_from( AssertionError("The test failed for %s" % test_property), e) def test_withholdFromItunesOffByDefault(self): assert not self.fg.withhold_from_itunes def test_withholdFromItunes(self): self.fg.withhold_from_itunes = True itunes_block = self.fg._create_rss().find("channel")\ .find("{%s}block" % self.nsItunes) assert itunes_block is not None self.assertEqual(itunes_block.text.lower(), "yes") self.fg.withhold_from_itunes = False itunes_block = self.fg._create_rss().find("channel")\ .find("{%s}block" % self.nsItunes) assert itunes_block is None def test_modifyingSkipDaysAfterwards(self): self.fg.skip_days.add("Unrecognized day") self.assertRaises(ValueError, self.fg.rss_str) self.fg.skip_days.remove("Unrecognized day") self.fg.rss_str() # Now it works def test_modifyingSkipHoursAfterwards(self): self.fg.skip_hours.add(26) self.assertRaises(ValueError, self.fg.rss_str) self.fg.skip_hours.remove(26) self.fg.rss_str() # Now it works # Tests for xslt def test_xslt_str(self): def use_str(**kwargs): return self.fg.rss_str(**kwargs) self.help_test_xslt_using(use_str) def test_xslt_file(self): def use_file(**kwargs): return self.getRssFeedFileContents(self.fg, **kwargs) self.help_test_xslt_using(use_file) def help_test_xslt_using(self, generated_feed): """Run tests for xslt, generating the feed str using the given function. """ xslt_path = "http://example.com/mystylesheet.xsl" xslt_pi = "<?xml-stylesheet" # No xslt when set to None self.fg.xslt = None assert xslt_pi not in generated_feed() assert xslt_pi not in generated_feed(minimize=True) assert xslt_pi not in generated_feed(xml_declaration=False) self.fg.xslt = xslt_path # Now we have the stylesheet in there assert xslt_pi in generated_feed() assert xslt_pi in generated_feed(minimize=True) assert xslt_pi in generated_feed(xml_declaration=False) assert xslt_path in generated_feed() assert xslt_path in generated_feed(minimize=True) assert xslt_path in generated_feed(xml_declaration=False) def test_imageWarningNoExt(self): with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") self.assertEqual(len(w), 0) # Set image to a URL without proper file extension no_ext = "http://static.example.com/images/logo" self.fg.image = no_ext # Did we get a warning? self.assertEqual(1, len(w)) assert issubclass(w.pop().category, NotSupportedByItunesWarning) # Was the image set? self.assertEqual(no_ext, self.fg.image) def test_imageWarningBadExt(self): with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") # Set image to a URL with an unsupported file extension bad_ext = "http://static.example.com/images/logo.gif" self.fg.image = bad_ext # Did we get a warning? self.assertEqual(1, len(w)) # Was it of the correct type? assert issubclass(w.pop().category, NotSupportedByItunesWarning) # Was the image still set? self.assertEqual(bad_ext, self.fg.image) def test_imageNoWarningWithGoodExt(self): with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") # Set image to a URL with a supported file extension extensions = ["jpg", "png", "jpeg"] for extension in extensions: good_ext = "http://static.example.com/images/logo." + extension self.fg.image = good_ext # Did we get no warning? self.assertEqual( 0, len(w), "Extension %s raised warnings (%s)" % (extension, w)) # Was the image set? self.assertEqual(good_ext, self.fg.image)
if '\n' in title: title = title.split('\n')[-1] episodes.append( Episode( title=title.title(), media=Media(media_url, head.headers["Content-Length"]), summary=soup.title.text, publication_date=arrow.get(date, "DD MMM. YYYY", locale="pt").datetime ) ) # Add some episodes p.episodes = episodes # Generate the RSS feed rss = p.rss_str() filename = "alta-tensao.xml" with open(filename, "wb") as f: f.write(rss.encode()) session = boto3.session.Session() client = session.client( 's3', region_name=REGION, endpoint_url=f'https://{REGION}.digitaloceanspaces.com', aws_access_key_id=ACCESS_KEY, aws_secret_access_key=ACCESS_SECRET )
class TestPodcast(unittest.TestCase): def setUp(self): fg = Podcast() self.nsContent = "http://purl.org/rss/1.0/modules/content/" self.nsDc = "http://purl.org/dc/elements/1.1/" self.nsItunes = "http://www.itunes.com/dtds/podcast-1.0.dtd" self.feed_url = "http://example.com/feeds/myfeed.rss" self.name = 'Some Testfeed' self.author = Person('John Doe', '*****@*****.**') self.website = 'http://example.com' self.description = 'This is a cool feed!' self.subtitle = 'Coolest of all' self.language = 'en' self.cloudDomain = 'example.com' self.cloudPort = '4711' self.cloudPath = '/ws/example' self.cloudRegisterProcedure = 'registerProcedure' self.cloudProtocol = 'SOAP 1.1' self.pubsubhubbub = "http://pubsubhubbub.example.com/" self.contributor = {'name':"Contributor Name", 'email': 'Contributor email'} self.copyright = "The copyright notice" self.docs = 'http://www.rssboard.org/rss-specification' self.skip_days = set(['Tuesday']) self.skip_hours = set([23]) self.explicit = False self.programname = podgen.version.name self.web_master = Person(email='*****@*****.**') self.image = "http://example.com/static/podcast.png" self.owner = self.author self.complete = True self.new_feed_url = "https://example.com/feeds/myfeed2.rss" self.xslt = "http://example.com/feed/stylesheet.xsl" fg.name = self.name fg.website = self.website fg.description = self.description fg.subtitle = self.subtitle fg.language = self.language fg.cloud = (self.cloudDomain, self.cloudPort, self.cloudPath, self.cloudRegisterProcedure, self.cloudProtocol) fg.pubsubhubbub = self.pubsubhubbub fg.copyright = self.copyright fg.authors.append(self.author) fg.skip_days = self.skip_days fg.skip_hours = self.skip_hours fg.web_master = self.web_master fg.feed_url = self.feed_url fg.explicit = self.explicit fg.image = self.image fg.owner = self.owner fg.complete = self.complete fg.new_feed_url = self.new_feed_url fg.xslt = self.xslt self.fg = fg warnings.simplefilter("always") def noop(*args, **kwargs): pass warnings.showwarning = noop def test_constructor(self): # Overwrite fg from setup self.fg = Podcast( name=self.name, website=self.website, description=self.description, subtitle=self.subtitle, language=self.language, cloud=(self.cloudDomain, self.cloudPort, self.cloudPath, self.cloudRegisterProcedure, self.cloudProtocol), pubsubhubbub=self.pubsubhubbub, copyright=self.copyright, authors=[self.author], skip_days=self.skip_days, skip_hours=self.skip_hours, web_master=self.web_master, feed_url=self.feed_url, explicit=self.explicit, image=self.image, owner=self.owner, complete=self.complete, new_feed_url=self.new_feed_url, xslt=self.xslt, ) # Test that the fields are actually set self.test_baseFeed() def test_constructorUnknownAttributes(self): self.assertRaises(TypeError, Podcast, naem="Oh, looks like a typo") self.assertRaises(TypeError, Podcast, "Haha, No Keyword") def test_baseFeed(self): fg = self.fg assert fg.name == self.name assert fg.authors[0] == self.author assert fg.web_master == self.web_master assert fg.website == self.website assert fg.description == self.description assert fg.subtitle == self.subtitle assert fg.language == self.language assert fg.feed_url == self.feed_url assert fg.image == self.image assert fg.owner == self.owner assert fg.complete == self.complete assert fg.pubsubhubbub == self.pubsubhubbub assert fg.cloud == (self.cloudDomain, self.cloudPort, self.cloudPath, self.cloudRegisterProcedure, self.cloudProtocol) assert fg.copyright == self.copyright assert fg.new_feed_url == self.new_feed_url assert fg.skip_days == self.skip_days assert fg.skip_hours == self.skip_hours assert fg.xslt == self.xslt def test_rssFeedFile(self): fg = self.fg rssString = self.getRssFeedFileContents(fg, xml_declaration=False)\ .replace('\n', '') self.checkRssString(rssString) def getRssFeedFileContents(self, fg, **kwargs): # Keep track of our temporary file and its filename filename = None file = None try: # Get our temporary file name file = tempfile.NamedTemporaryFile(delete=False) filename = file.name # Close the file; we will just use its name file.close() # Write the RSS to the file (overwriting it) fg.rss_file(filename=filename, **kwargs) # Read the resulting RSS with open(filename, "r") as myfile: rssString = myfile.read() finally: # We don't need the file any longer, so delete it if filename: os.unlink(filename) elif file: # Ops, we were interrupted between the first and second stmt filename = file.name file.close() os.unlink(filename) else: # We were interrupted between entering the try-block and # getting the temporary file. Not much we can do. pass return rssString def test_rssFeedString(self): fg = self.fg rssString = fg.rss_str(xml_declaration=False) self.checkRssString(rssString) def test_rssStringAndFileAreEqual(self): rss_string = self.fg.rss_str() rss_file = self.getRssFeedFileContents(self.fg) self.assertEqual(rss_string, rss_file) def checkRssString(self, rssString): feed = etree.fromstring(rssString) nsRss = self.nsContent nsAtom = "http://www.w3.org/2005/Atom" channel = feed.find("channel") assert channel != None assert channel.find("title").text == self.name assert channel.find("description").text == self.description assert channel.find("{%s}subtitle" % self.nsItunes).text == \ self.subtitle assert channel.find("link").text == self.website assert channel.find("lastBuildDate").text != None assert channel.find("language").text == self.language assert channel.find("docs").text == "http://www.rssboard.org/rss-specification" assert self.programname in channel.find("generator").text assert channel.find("cloud").get('domain') == self.cloudDomain assert channel.find("cloud").get('port') == self.cloudPort assert channel.find("cloud").get('path') == self.cloudPath assert channel.find("cloud").get('registerProcedure') == self.cloudRegisterProcedure assert channel.find("cloud").get('protocol') == self.cloudProtocol assert channel.find("copyright").text == self.copyright assert channel.find("docs").text == self.docs assert self.author.email in channel.find("managingEditor").text assert channel.find("skipDays").find("day").text in self.skip_days assert int(channel.find("skipHours").find("hour").text) in self.skip_hours assert self.web_master.email in channel.find("webMaster").text links = channel.findall("{%s}link" % nsAtom) selflinks = [link for link in links if link.get('rel') == 'self'] hublinks = [link for link in links if link.get('rel') == 'hub'] assert selflinks, "No <atom:link rel='self'> element found" selflink = selflinks[0] assert selflink.get('href') == self.feed_url assert selflink.get('type') == 'application/rss+xml' assert hublinks, "No <atom:link rel='hub'> element found" hublink = hublinks[0] assert hublink.get('href') == self.pubsubhubbub assert hublink.get('type') is None assert channel.find("{%s}image" % self.nsItunes).get('href') == \ self.image owner = channel.find("{%s}owner" % self.nsItunes) assert owner.find("{%s}name" % self.nsItunes).text == self.owner.name assert owner.find("{%s}email" % self.nsItunes).text == self.owner.email assert channel.find("{%s}complete" % self.nsItunes).text.lower() == \ "yes" assert channel.find("{%s}new-feed-url" % self.nsItunes).text == \ self.new_feed_url def test_feedUrlValidation(self): self.assertRaises(ValueError, setattr, self.fg, "feed_url", "example.com/feed.rss") def test_generator(self): software_name = "My Awesome Software" software_version = (1, 0) software_url = "http://example.com/awesomesoft/" # Using set_generator, text includes python-podgen self.fg.set_generator(software_name) rss = self.fg._create_rss() generator = rss.find("channel").find("generator").text assert software_name in generator assert self.programname in generator # Using set_generator, text excludes python-podgen self.fg.set_generator(software_name, exclude_podgen=True) generator = self.fg._create_rss().find("channel").find("generator").text assert software_name in generator assert self.programname not in generator # Using set_generator, text includes name, version and url self.fg.set_generator(software_name, software_version, software_url) generator = self.fg._create_rss().find("channel").find("generator").text assert software_name in generator assert str(software_version[0]) in generator assert str(software_version[1]) in generator assert software_url in generator # Using generator directly, text excludes python-podgen self.fg.generator = software_name generator = self.fg._create_rss().find("channel").find("generator").text assert software_name in generator assert self.programname not in generator def test_str(self): assert str(self.fg) == self.fg.rss_str( minimize=False, encoding="UTF-8", xml_declaration=True ) def test_updated(self): date = datetime.datetime(2016, 1, 1, 0, 10, tzinfo=dateutil.tz.tzutc()) def getLastBuildDateElement(fg): return fg._create_rss().find("channel").find("lastBuildDate") # Test that it has a default assert getLastBuildDateElement(self.fg) is not None # Test that it respects my custom value self.fg.last_updated = date lastBuildDate = getLastBuildDateElement(self.fg) assert lastBuildDate is not None assert dateutil.parser.parse(lastBuildDate.text) == date # Test that it is left out when set to False self.fg.last_updated = False lastBuildDate = getLastBuildDateElement(self.fg) assert lastBuildDate is None def test_AuthorEmail(self): # Just email - so use managingEditor, not dc:creator or itunes:author # This is per the RSS best practices, see the section about dc:creator self.fg.authors = [Person(None, "*****@*****.**")] channel = self.fg._create_rss().find("channel") # managingEditor uses email? assert channel.find("managingEditor").text == self.fg.authors[0].email # No dc:creator? assert channel.find("{%s}creator" % self.nsDc) is None # No itunes:author? assert channel.find("{%s}author" % self.nsItunes) is None def test_AuthorName(self): # Just name - use dc:creator and itunes:author, not managingEditor self.fg.authors = [Person("Just a. Name")] channel = self.fg._create_rss().find("channel") # No managingEditor? assert channel.find("managingEditor") is None # dc:creator equals name? assert channel.find("{%s}creator" % self.nsDc).text == \ self.fg.authors[0].name # itunes:author equals name? assert channel.find("{%s}author" % self.nsItunes).text == \ self.fg.authors[0].name def test_AuthorNameAndEmail(self): # Both name and email - use managingEditor and itunes:author, # not dc:creator self.fg.authors = [Person("Both a name", "*****@*****.**")] channel = self.fg._create_rss().find("channel") # Does managingEditor follow the pattern "email (name)"? self.assertEqual(self.fg.authors[0].email + " (" + self.fg.authors[0].name + ")", channel.find("managingEditor").text) # No dc:creator? assert channel.find("{%s}creator" % self.nsDc) is None # itunes:author uses name only? assert channel.find("{%s}author" % self.nsItunes).text == \ self.fg.authors[0].name def test_multipleAuthors(self): # Multiple authors - use itunes:author and dc:creator, not # managingEditor. person1 = Person("Multiple", "*****@*****.**") person2 = Person("Are", "*****@*****.**") self.fg.authors = [person1, person2] channel = self.fg._create_rss().find("channel") # Test dc:creator author_elements = \ channel.findall("{%s}creator" % self.nsDc) author_texts = [e.text for e in author_elements] assert len(author_texts) == 2 assert person1.name in author_texts[0] assert person1.email in author_texts[0] assert person2.name in author_texts[1] assert person2.email in author_texts[1] # Test itunes:author itunes_author = channel.find("{%s}author" % self.nsItunes) assert itunes_author is not None itunes_author_text = itunes_author.text assert person1.name in itunes_author_text assert person1.email not in itunes_author_text assert person2.name in itunes_author_text assert person2.email not in itunes_author_text # Test that managingEditor is not used assert channel.find("managingEditor") is None def test_authorsInvalidValue(self): self.assertRaises(TypeError, self.do_authorsInvalidValue) def do_authorsInvalidValue(self): self.fg.authors = Person("Opsie", "*****@*****.**") def test_webMaster(self): self.fg.web_master = Person(None, "*****@*****.**") channel = self.fg._create_rss().find("channel") assert channel.find("webMaster").text == self.fg.web_master.email self.assertRaises(ValueError, setattr, self.fg, "web_master", Person("Mr. No Email Address")) self.fg.web_master = Person("Both a name", "*****@*****.**") channel = self.fg._create_rss().find("channel") # Does webMaster follow the pattern "email (name)"? self.assertEqual(self.fg.web_master.email + " (" + self.fg.web_master.name + ")", channel.find("webMaster").text) def test_categoryWithoutSubcategory(self): c = Category("Arts") self.fg.category = c channel = self.fg._create_rss().find("channel") itunes_category = channel.find("{%s}category" % self.nsItunes) assert itunes_category is not None self.assertEqual(itunes_category.get("text"), c.category) assert itunes_category.find("{%s}category" % self.nsItunes) is None def test_categoryWithSubcategory(self): c = Category("Arts", "Food") self.fg.category = c channel = self.fg._create_rss().find("channel") itunes_category = channel.find("{%s}category" % self.nsItunes) assert itunes_category is not None itunes_subcategory = itunes_category\ .find("{%s}category" % self.nsItunes) assert itunes_subcategory is not None self.assertEqual(itunes_subcategory.get("text"), c.subcategory) def test_categoryChecks(self): c = ("Arts", "Food") self.assertRaises(TypeError, setattr, self.fg, "category", c) def test_explicitIsExplicit(self): self.fg.explicit = True channel = self.fg._create_rss().find("channel") itunes_explicit = channel.find("{%s}explicit" % self.nsItunes) assert itunes_explicit is not None assert itunes_explicit.text.lower() in ("yes", "explicit", "true"),\ "itunes:explicit was %s, expected yes, explicit or true" \ % itunes_explicit.text def test_explicitIsClean(self): self.fg.explicit = False channel = self.fg._create_rss().find("channel") itunes_explicit = channel.find("{%s}explicit" % self.nsItunes) assert itunes_explicit is not None assert itunes_explicit.text.lower() in ("no", "clean", "false"),\ "itunes:explicit was %s, expected no, clean or false" \ % itunes_explicit.text def test_mandatoryValues(self): # Try to create a Podcast once for each mandatory property. # On each iteration, exactly one of the properties is not set. # Therefore, an exception should be thrown on each iteration. mandatory_properties = set([ "description", "title", "link", "explicit", ]) for test_property in mandatory_properties: fg = Podcast() if test_property != "description": fg.description = self.description if test_property != "title": fg.name = self.name if test_property != "link": fg.website = self.website if test_property != "explicit": fg.explicit = self.explicit try: self.assertRaises(ValueError, fg._create_rss) except AssertionError as e: raise_from(AssertionError( "The test failed for %s" % test_property), e) def test_withholdFromItunesOffByDefault(self): assert not self.fg.withhold_from_itunes def test_withholdFromItunes(self): self.fg.withhold_from_itunes = True itunes_block = self.fg._create_rss().find("channel")\ .find("{%s}block" % self.nsItunes) assert itunes_block is not None self.assertEqual(itunes_block.text.lower(), "yes") self.fg.withhold_from_itunes = False itunes_block = self.fg._create_rss().find("channel")\ .find("{%s}block" % self.nsItunes) assert itunes_block is None def test_modifyingSkipDaysAfterwards(self): self.fg.skip_days.add("Unrecognized day") self.assertRaises(ValueError, self.fg.rss_str) self.fg.skip_days.remove("Unrecognized day") self.fg.rss_str() # Now it works def test_modifyingSkipHoursAfterwards(self): self.fg.skip_hours.add(26) self.assertRaises(ValueError, self.fg.rss_str) self.fg.skip_hours.remove(26) self.fg.rss_str() # Now it works # Tests for xslt def test_xslt_str(self): def use_str(**kwargs): return self.fg.rss_str(**kwargs) self.help_test_xslt_using(use_str) def test_xslt_file(self): def use_file(**kwargs): return self.getRssFeedFileContents(self.fg, **kwargs) self.help_test_xslt_using(use_file) def help_test_xslt_using(self, generated_feed): """Run tests for xslt, generating the feed str using the given function. """ xslt_path = "http://example.com/mystylesheet.xsl" xslt_pi = "<?xml-stylesheet" # No xslt when set to None self.fg.xslt = None assert xslt_pi not in generated_feed() assert xslt_pi not in generated_feed(minimize=True) assert xslt_pi not in generated_feed(xml_declaration=False) self.fg.xslt = xslt_path # Now we have the stylesheet in there assert xslt_pi in generated_feed() assert xslt_pi in generated_feed(minimize=True) assert xslt_pi in generated_feed(xml_declaration=False) assert xslt_path in generated_feed() assert xslt_path in generated_feed(minimize=True) assert xslt_path in generated_feed(xml_declaration=False) def test_imageWarningNoExt(self): with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") self.assertEqual(len(w), 0) # Set image to a URL without proper file extension no_ext = "http://static.example.com/images/logo" self.fg.image = no_ext # Did we get a warning? self.assertEqual(1, len(w)) assert issubclass(w.pop().category, NotSupportedByItunesWarning) # Was the image set? self.assertEqual(no_ext, self.fg.image) def test_imageWarningBadExt(self): with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") # Set image to a URL with an unsupported file extension bad_ext = "http://static.example.com/images/logo.gif" self.fg.image = bad_ext # Did we get a warning? self.assertEqual(1, len(w)) # Was it of the correct type? assert issubclass(w.pop().category, NotSupportedByItunesWarning) # Was the image still set? self.assertEqual(bad_ext, self.fg.image) def test_imageNoWarningWithGoodExt(self): with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") # Set image to a URL with a supported file extension extensions = ["jpg", "png", "jpeg"] for extension in extensions: good_ext = "http://static.example.com/images/logo." + extension self.fg.image = good_ext # Did we get no warning? self.assertEqual(0, len(w), "Extension %s raised warnings (%s)" % (extension, w)) # Was the image set? self.assertEqual(good_ext, self.fg.image)
def create_rss(type, download): """Create an example podcast and print it or save it to a file.""" # Create the Podcast & initialize the feed default_channel = Channel.defaultChannel() p = Podcast() p.name = default_channel.name p.description = default_channel.description p.website = default_channel.website p.explicit = default_channel.explicit p.image = default_channel.image p.copyright = default_channel.copyright p.language = default_channel.language p.feed_url = default_channel.feed_url p.category = Category(default_channel.category) # p.category = Category('Technology', 'Podcasting') # p.xslt = "https://example.com/feed/stylesheet.xsl" # URL of XSLT stylesheet p.authors = [Person(default_channel.authors, default_channel.authors_email)] p.owner = Person(default_channel.owner, default_channel.owner_email) # Other Attributes p.generator = " " # Others for iTunes # p.complete = False # p.new_feed_url = 'http://example.com/new-feed.rss' # e1 = p.add_episode() # e1.id = 'http://lernfunk.de/_MEDIAID_123#1' # e1.title = 'First Element' # e1.summary = htmlencode('''Lorem ipsum dolor sit amet, consectetur adipiscing elit. Tamen # aberramus a proposito, et, ne longius, prorsus, inquam, Piso, si ista # mala sunt, placet. Aut etiam, ut vestitum, sic sententiam habeas aliam # domesticam, aliam forensem, ut in fronte ostentatio sit, intus veritas # occultetur? Cum id fugiunt, re eadem defendunt, quae Peripatetici, # verba <3.''') # e1.link = 'http://example.com' # e1.authors = [Person('Lars Kiesow', '*****@*****.**')] # e1.publication_date = datetime.datetime(2014, 5, 17, 13, 37, 10, tzinfo=pytz.utc) # # e1.media = Media("http://example.com/episodes/loremipsum.mp3", 454599964, # # duration= # # datetime.timedelta(hours=1, minutes=32, seconds=19)) # e1.media = Media("http://example.com/episodes/loremipsum.mp3", 454599964) # Add some episodes p.episodes += [ Episode(title = download.title, subtitle = download.subtitle, # id=str(uuid.uuid4()), position =2, media = Media(download.media_url, size=download.media_size, duration=timedelta(seconds=download.media_duration)), image = download.image_url, publication_date = datetime(year=2021, month=1, day=8, hour=10, minute=0, tzinfo=pytz.utc), summary = download.summary) , Episode(title="Episode 2 - The Crazy Ones", subtitle="this is a cool episode, this is for th crazy ones", position=1, image="https://github.com/oliverbarreto/PersonalPodcast/raw/main/site-logo-1400x1400.png", media=Media("https://github.com/oliverbarreto/PersonalPodcast/raw/main/downloaded_with_pytube_Apple%20Steve%20Jobs%20Heres%20To%20The%20Crazy%20Ones.mp4", type="audio/mpeg", size=989, duration=timedelta(hours=0, minutes=1, seconds=1)), publication_date = datetime(year=2021, month=1, day=6, hour=10, minute=0, tzinfo=pytz.utc), summary=htmlencode("wow wow wow summary")) , Episode(title="Episode 3 - The Super Crazy", subtitle="crazy ones revisited", position=0, image="https://github.com/oliverbarreto/PersonalPodcast/raw/main/site-logo-1400x1400.png", media=Media("https://drive.google.com/file/d/1X5Mwa8V0Su1IDqhcQL7LdzEY0VaMC1Nn", type="audio/mpeg", size=989, duration=timedelta(hours=0, minutes=1, seconds=1)), publication_date = datetime(year=2021, month=1, day=10, hour=10, minute=0, tzinfo=pytz.utc), summary=download.summary) ] # Should we just print out, or write to file? if type == 'print': # Print print_enc(p.rss_str()) elif type== 'feed.xml': # Write to file p.rss_file(type, minimize=False) print("\n") print("feed.xml created !!!")