diff --git a/doc/_static/custom.css b/doc/_static/custom.css
index d648527..9c8d85e 100644
--- a/doc/_static/custom.css
+++ b/doc/_static/custom.css
@@ -4,3 +4,18 @@ body, div.body {
pre {
background-color: #ebebeb;
}
+div.sphinxsidebar a.current {
+ text-decoration: none;
+ border-bottom: none;
+ font-weight: bold;
+ cursor: text;
+}
+/* Don't hide logo when viewed on mobile, just invert its colors */
+@media screen and (max-width: 875px) {
+ div.sphinxsidebar p.logo {
+ display: block;
+ filter: invert(100%);
+ width: 60%;
+ margin: auto;
+ }
+}
diff --git a/doc/api.rst b/doc/api.rst
index a611d6d..a535034 100644
--- a/doc/api.rst
+++ b/doc/api.rst
@@ -1,28 +1,6 @@
=================
-Developer's Guide
-=================
-
-
--------
-Testing
--------
-
-You can test the module integration-testing-style by simply executing::
-
- $ python -m podgen
-
-When working on this project, you should run the unit tests as well as the
-integration test, like this::
-
- $ make test
-
-The unit tests reside in ``podgen/tests`` and are written using the
-:mod:`unittest` module.
-
-
------------------
API Documentation
------------------
+=================
.. autosummary::
diff --git a/doc/conf.py b/doc/conf.py
index 49a572d..37953c4 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -16,7 +16,10 @@
# -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
-#needs_sphinx = '1.0'
+needs_sphinx = '1.4'
+
+# Don't show warnings about the button images not being local
+suppress_warnings = ['image.nonlocal_uri']
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
@@ -103,12 +106,11 @@
'fixed_sidebar': False,
'page_width': "1000px",
'sidebar_width': "225px",
- 'show_related': True,
'body_text': "rgba(0, 0, 0, 0.8)",
'footer_text': "rgba(0, 0, 0, 0.5)",
'gray_1': "rgba(0, 0, 0, 0.9)",
'gray_2': "rgba(0, 0, 0, 0.2)",
- 'gray_3': "rgba(0, 0, 0, 0.1)",
+ 'gray_3': "rgba(198, 198, 198, 0.9)",
'github_user': 'tobinus',
'github_repo': 'python-podgen',
@@ -134,7 +136,7 @@
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
-html_favicon = "favicon.ico"
+html_favicon = "_static/favicon.ico"
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
@@ -215,13 +217,13 @@
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
- ('index', 'pyPodGen.tex', u'pyPodGen Documentation',
- u'Lars Kiesow', 'manual'),
+ ('index', 'pyPodGen.tex', u'PodGen Documentation',
+ u'Lars Kiesow and Thorben Dahl', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
-#latex_logo = None
+latex_logo = '_static/logo.png'
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
@@ -231,13 +233,13 @@
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
-#latex_show_urls = False
+latex_show_urls = "true"
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
-#latex_domain_indices = True
+latex_domain_indices = False
# -- Options for manual page output --------------------------------------------
@@ -250,7 +252,7 @@
]
# If true, show URL addresses after external links.
-#man_show_urls = False
+man_show_urls = True
# -- Options for Texinfo output ------------------------------------------------
@@ -275,7 +277,7 @@
# Example configuration for intersphinx: refer to the Python standard library.
-intersphinx_mapping = {'http://docs.python.org/': None}
+intersphinx_mapping = {'python': ('https://docs.python.org/3', None)}
# Ugly way of setting tabsize
diff --git a/doc/contributing.rst b/doc/contributing.rst
new file mode 100644
index 0000000..8620b47
--- /dev/null
+++ b/doc/contributing.rst
@@ -0,0 +1,92 @@
+============
+Contributing
+============
+
+Setting up
+----------
+
+To install the dependencies, run::
+
+ $ pip install -r requirements
+
+while you have a `virtual environment `_
+activated.
+
+You are recommended to use `pyenv `_ to handle
+virtual environments and Python versions. That way, you can easily test and
+debug problems that are specific to one version of Python.
+
+Testing
+-------
+
+You can perform an integration test by running ``podgen/__main__.py``::
+
+ $ python -m podgen
+
+When working on this project, you should run the unit tests as well as the
+integration test, like this::
+
+ $ make test
+
+The unit tests reside in ``podgen/tests`` and are written using the
+:mod:`unittest` module.
+
+
+Values
+------
+
+Read :doc:`/user/introduction` and :doc:`/user/fork` for a run-down on what
+values/principles lay the foundation for this project. In short, it is important
+to keep the API as simple as possible.
+
+You must also write unittests as you code, ideally using **test-driven
+development** (that is, write a test, observe that the test fails, write code
+so the test works, observe that the test succeeds, write a new test and so on).
+That way, you know that the tests actually contribute and you get to think
+about how the API will look before you tackle the problem head-on.
+
+Make sure you update ``podgen/__main__.py`` so it still works, and use your new
+functionality there if it makes sense.
+
+You must also make sure you **update any relevant documentation**. Remember that
+the documentation includes lots of examples and also describes the API
+independently from docstring comments in the code itself.
+
+Pull requests in which the unittests and documentation are NOT up to date
+with the code will NOT be accepted.
+
+Lastly, a single **commit** shouldn't include more changes than it needs. It's better to do a big
+change in small steps, each of which is one commit. Explain the impact of your
+changes in the commit message.
+
+The Workflow
+------------
+
+#. Check out `waffle.io `_ or
+ `GitHub Issues `_.
+
+ * Find the issue you wish to work on.
+ * Add your issue if it's not already there.
+ * Discuss the issue and get feedback on your proposed solution. Don't waste
+ time on a solution that might not be accepted!
+
+#. Work on the issue in a separate branch which follows the name scheme
+ ``tobinus/python-podgen#-`` in your own fork. To be honest, I
+ don't know if Waffle.io will notice that, but it doesn't hurt to try, I
+ guess! You might want to read up on `Waffle.io's recommended workflow `_.
+
+#. Push the branch.
+
+#. Do the work.
+
+#. When you're done and you've updated the documentation and tests (see above),
+ create a pull request which references the issue.
+
+#. Wait for me or some other team member to review the pull request. Keep an
+ eye on your inbox or your GitHub notifications, since we may have some
+ objections or feedback that you must take into consideration. **It'd be a
+ shame if your work never led to anything because you didn't notice a
+ comment!**
+
+#. Consider making the same changes to `python-feedgen `_
+ as well.
diff --git a/doc/index.rst b/doc/index.rst
index 6f38df4..5e01b6b 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -9,46 +9,53 @@ PodGen
:target: http://podgen.readthedocs.io/en/latest/?badge=latest
:alt: Documentation Status
-Wouldn't it be nice if there was a **clean and simple library** which could help you
-**generate podcast RSS feeds** from your Python code? Well, today's your lucky day!
+.. image:: https://badge.waffle.io/tobinus/python-podgen.svg?label=ready&title=Ready
+ :target: https://waffle.io/tobinus/python-podgen
+ :alt: 'Stories in Ready'
+
+Don't you wish there was a **clean and simple library** which could help you
+**generate podcast RSS feeds** with your Python code? Well, today's your lucky day!
>>> from podgen import Podcast, Episode, Media
>>> # Create the Podcast
>>> p = Podcast(
- name="My Awesome Podcast",
+ name="The Library Tuesday Talk",
description="My friends and I discuss Python"
" libraries each Tuesday!",
- website="http://example.org/awesomepodcast"
+ website="http://example.org/librarytuesdaytalk"
)
>>> # Add some episodes
>>> p.episodes += [
- Episode(title="PodGen rocks!",
+ Episode(title="Worry about timezones no more",
media=Media("http://example.org/ep1.mp3", 11932295),
- summary="I found an awesome library for creating podcasts"),
+ summary="Using pytz, you can make your code timezone-aware "
+ "with very little hassle."),
Episode(title="Heard about clint?",
media=Media("http://example.org/ep2.mp3", 15363464),
summary="The man behind Requests made something useful "
- "for us command-line lovers."
+ "for us command-line nerds."
]
>>> # Generate the RSS feed
>>> rss = str(p)
You don't need to read the RSS specification, write XML by hand or wrap your
-head around ambiguous, undocumented APIs. Just provide the data, and PodGen
-fixes the rest for you!
-
-Where to start
---------------
+head around ambiguous, undocumented APIs. PodGen incorporates the industry's
+best practices and lets you focus on collecting the necessary metadata and
+publishing the podcast.
-Take a look at the :doc:`user/example` for a larger example, read about
-:doc:`the project's background ` or refer to
-the :doc:`user/basic_usage_guide/index` for a detailed introduction to PodGen.
-Contents
---------
+User Guide
+----------
.. toctree::
:maxdepth: 3
- user/index
+ user/introduction
+ user/installation
+ user/fork
+ user/basic_usage_guide/part_1
+ user/basic_usage_guide/part_2
+ user/basic_usage_guide/part_3
+ user/example
+ contributing
api
diff --git a/doc/user/basic_usage_guide/index.rst b/doc/user/basic_usage_guide/index.rst
deleted file mode 100644
index 24157b1..0000000
--- a/doc/user/basic_usage_guide/index.rst
+++ /dev/null
@@ -1,18 +0,0 @@
-Basic usage guide
-=================
-
-When using PodGen, you can divide your program into
-three phases:
-
-.. toctree::
- :maxdepth: 1
-
- part_1
- part_2
- part_3
-
-While the
-:doc:`../example` gives you a practical introduction, this document helps you
-understand what the different attributes mean and how they should be used.
-It complements the :doc:`/api` nicely.
-
diff --git a/doc/user/basic_usage_guide/part_1.rst b/doc/user/basic_usage_guide/part_1.rst
index e87c6c7..4d52cd8 100644
--- a/doc/user/basic_usage_guide/part_1.rst
+++ b/doc/user/basic_usage_guide/part_1.rst
@@ -1,5 +1,5 @@
-Populating the podcast
-----------------------
+Creating the podcast
+--------------------
Creating a new instance
~~~~~~~~~~~~~~~~~~~~~~~
@@ -9,7 +9,7 @@ Creating a new instance
from podgen import Podcast
p = Podcast()
-Mandatory properties
+Mandatory attributes
~~~~~~~~~~~~~~~~~~~~
::
@@ -19,12 +19,12 @@ Mandatory properties
p.website = "https://example.org"
p.explicit = True
-Those four properties, :attr:`~podgen.Podcast.name`,
-:attr:`~podgen.Podcast.description`,
-:attr:`~podgen.Podcast.explicit` and
-:attr:`~podgen.Podcast.website`, are actually
-the only four **mandatory** properties of
-:class:`~podgen.Podcast`.
+They're self explanatory, but you can read more about them if you'd like:
+
+* :attr:`~podgen.Podcast.name`
+* :attr:`~podgen.Podcast.description`
+* :attr:`~podgen.Podcast.website`
+* :attr:`~podgen.Podcast.explicit`
Image
~~~~~
@@ -38,10 +38,10 @@ A podcast's image is worth special attention::
Even though the image *technically* is optional, you won't reach people without it.
-Optional properties
+Optional attributes
~~~~~~~~~~~~~~~~~~~
-There are plenty of other properties that can be used with
+There are plenty of other attributes that can be used with
:class:`podgen.Podcast `:
@@ -53,7 +53,7 @@ Commonly used
p.copyright = "2016 Example Radio"
p.language = "en-US"
p.authors = [Person("John Doe", "editor@example.org")]
- p.feed_url = "https://example.com/feeds/podcast.rss"
+ p.feed_url = "https://example.com/feeds/podcast.rss" # URL of this feed
p.category = Category("Technology", "Podcasting")
p.owner = p.authors[0]
@@ -75,20 +75,51 @@ again have very reasonable defaults.
::
+ # RSS Cloud enables podcatchers to subscribe to notifications when there's
+ # a new episode ready, however it's not used much.
p.cloud = ("server.example.com", 80, "/rpc", "cloud.notify", "xml-rpc")
import datetime
+ # pytz is a dependency of this library, and makes it easy to deal with
+ # timezones. Generally, all dates must be timezone aware.
import pytz
+ # last_updated is datetime when the feed was last refreshed. If you don't
+ # set it, the current date and time will be used instead when the feed is
+ # generated, which is generally what you want. Nevertheless, you can
+ # set your own date:
p.last_updated = datetime.datetime(2016, 5, 18, 0, 0, tzinfo=pytz.utc))
- p.publication_date = datetime.datetime(2016, 5, 17, 15, 32, tzinfo=pytz.utc))
+ # publication_date is when the contents of this feed last were published.
+ # If you don't set it, the date of the most recent Episode is used. Again,
+ # this is generally what you want, but you can override it:
+ p.publication_date = datetime.datetime(2016, 5, 17, 15, 32,tzinfo=pytz.utc))
+
+ # Set of days on which podcatchers won't need to refresh the feed.
+ # Not implemented widely.
p.skip_days = {"Friday", "Saturday", "Sunday"}
+
+ # Set of hours on which podcatchers won't need to refresh the feed.
+ # Not implemented widely.
p.skip_hours = set(range(8))
p.skip_hours |= set(range(16, 24))
+
+ # Person to contact regarding technical aspects of the feed.
p.web_master = Person(None, "helpdesk@dallas.example.com")
- # Be very careful about using the following attributes:
+
+ # Identify the software which generates the feed (defaults to python-podgen)
+ p.set_generator("ExamplePodcastProgram", (1,0,0))
+ # (you can also set the generator string directly)
+ p.generator = "ExamplePodcastProgram v1.0.0 (with help from python-feedgen)"
+
+ # !!! Be very careful about using the following attributes !!!
+
+ # Tell iTunes that this feed has moved somewhere else.
p.new_feed_url = "https://podcast.example.com/example"
+
+ # Tell iTunes that this feed will never be updated again.
p.complete = True
+
+ # Tell iTunes that you'd rather not have this feed appear on iTunes.
p.withhold_from_itunes = True
Read more:
@@ -99,12 +130,13 @@ Read more:
* :attr:`~podgen.Podcast.skip_days`
* :attr:`~podgen.Podcast.skip_hours`
* :attr:`~podgen.Podcast.web_master`
+* :meth:`~podgen.Podcast.set_generator`
* :attr:`~podgen.Podcast.new_feed_url`
* :attr:`~podgen.Podcast.complete`
* :attr:`~podgen.Podcast.withhold_from_itunes`
Shortcut for filling in data
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Instead of creating a new :class:`.Podcast` object in one statement, and
populating it with data one statement at a time afterwards, you can create a
@@ -118,6 +150,8 @@ use the attribute name as keyword arguments to the constructor::
...
)
+Using this technique, you can define the Podcast as part of a list
+comprehension, dictionaries and so on.
Take a look at the :doc:`API Documentation for Podcast ` for a
practical example.
diff --git a/doc/user/basic_usage_guide/part_2.rst b/doc/user/basic_usage_guide/part_2.rst
index 62d920d..c2fcbaa 100644
--- a/doc/user/basic_usage_guide/part_2.rst
+++ b/doc/user/basic_usage_guide/part_2.rst
@@ -4,25 +4,33 @@ Adding episodes
To add episodes to a feed, you need to create new
:class:`podgen.Episode` objects and
-append them to the list of entries in the Podcast. That is pretty
+append them to the list of episodes in the Podcast. That is pretty
straight-forward::
- from podgen import Episode
+ from podgen import Podcast, Episode
+ # Create the podcast (see the previous section)
+ p = Podcast()
+ # Create new episode
my_episode = Episode()
+ # Add it to the podcast
p.episodes.append(my_episode)
There is a convenience method called :meth:`Podcast.add_episode `
which optionally creates a new instance of :class:`~podgen.Episode`, adds it to the podcast
and returns it, allowing you to assign it to a variable::
+ from podgen import Podcast
+ p = Podcast()
my_episode = p.add_episode()
If you prefer to use the constructor, there's nothing wrong with that::
+ from podgen import Podcast, Episode
+ p = Podcast()
my_episode = p.add_episode(Episode())
-The advantage of using the latter form, is that you can pass data to the
-constructor, which can make your code more compact and readable.
+The advantage of using the latter form is that you can pass data to the
+constructor.
Filling with data
~~~~~~~~~~~~~~~~~
@@ -63,14 +71,26 @@ Of course, this isn't much of a podcast if we don't have any
duration=timedelta(hours=1, minutes=2, seconds=36)
)
-Normally, you must specify how big the **file size** is in bytes (and the MIME
-type, if the file extension is unknown to iTunes), but PodcastGenerator
+The **type** of the media file is derived from the URI ending, if you don't
+provide it yourself. Even though you
+technically can have file names which don't end in their actual file extension,
+iTunes will use the file extension to determine what type of file it is, without
+even asking the server. You must therefore make sure your media files have the
+correct file extension. If you don't care about compatibility with iTunes, you
+can provide the MIME type yourself.
+
+The **duration** is also important to include for your listeners' convenience.
+Without it, they won't know how long an episode is before they start downloading
+and listening. It must be an instance of :class:`datetime.timedelta`.
+
+Normally, you must specify how big the **file size** is in bytes (and the `MIME
+type`_ if the file extension is unknown to iTunes), but PodGen
can send a HEAD request to the URL and retrieve the missing information
-(file size and type). This is done by calling
+(both file size and type). This is done by calling
:meth:`Media.create_from_server_response `
instead of using the constructor directly.
You must pass in the `requests `_
-module, so it must be installed! ::
+module, so make sure it's installed. ::
import requests
my_episode.media = Media.create_from_server_response(
@@ -79,27 +99,16 @@ module, so it must be installed! ::
duration=timedelta(hours=1, minutes=2, seconds=36)
)
+.. note::
-The **type** of the media file is derived from the URI ending, if you don't
-provide it yourself. Even though you
-technically can have file names which don't end in their actual file extension,
-iTunes will use the file extension to determine what type of file it is, without
-even asking the server. You must therefore make sure your media files have the
-correct file extension. If you don't care about compatibility with iTunes, you
-can provide the MIME type yourself.
-:meth:`Media.create_from_server_response `
-will also fetch the type for you, if it's not specified.
-
-The **duration** is also important to include, for your listeners' convenience.
-Without it, they won't know how long an episode is before they start downloading
-and listening. The duration cannot be fetched from the server automatically, and
-must be an instance of :class:`datetime.timedelta`.
+ The duration cannot be fetched from the server automatically.
Read more about:
* :attr:`podgen.Episode.media` (the attribute)
* :class:`podgen.Media` (the class which you use as value)
+.. _MIME type: https://en.wikipedia.org/wiki/Media_type
Identifying the episode
^^^^^^^^^^^^^^^^^^^^^^^
@@ -129,16 +138,15 @@ will get a new episode which appears to have existed for longer than it has.
.. note::
It is generally a bad idea to use the media file's modification date
- as the publication date when you make your episodes some time in advance
- – your listeners will suddenly get an "old" episode in
- their feed!
+ as the publication date. If you make your episodes some time in advance, your
+ listeners will suddenly get an "old" episode in their feed!
::
my_episode.publication_date = datetime.datetime(2016, 5, 18, 10, 0,
tzinfo=pytz.utc)
-Read more about :attr:`the publication_date attribute `.
+Read more about :attr:`the publication_date attribute `.
The Link
@@ -163,20 +171,13 @@ Read more about :attr:`the link attribute `.
The Authors
^^^^^^^^^^^
-.. note::
-
- Some of the following attributes (not just authors) correspond to attributes
- found in :class:`~podgen.Podcast`. In such cases, you should only set those
- attributes at the episode level if they **differ** from their value at the
- podcast level.
-
Normally, the attributes :attr:`Podcast.authors `
and :attr:`Podcast.web_master ` (if set) are
used to determine the authors of an episode. Thus, if all your episodes have
the same authors, you should just set it at the podcast level.
-If an episode's authors differs from the podcast's, though, you can override it
-like this::
+If an episode's list of authors differs from the podcast's, though, you can
+override it like this::
my_episode.authors = [Person("Joe Bob")]
@@ -192,11 +193,20 @@ Less used attributes
::
+ # Not actually implemented by iTunes; the Podcast's image is used.
my_episode.image = "http://example.com/static/best-example.png"
+
+ # Set it to override the Podcast's explicit attribute for this episode only.
my_episode.explicit = False
- my_episode.is_closed_captioned = False # Only applicable for video
+
+ # Tell iTunes that the enclosed video is closed captioned.
+ my_episode.is_closed_captioned = False
+
+ # Tell iTunes that this episode should be the first episode on the store
+ # page.
my_episode.position = 1
- # Be careful about using the following attribute!
+
+ # Careful! This will hide this episode from the iTunes store page.
my_episode.withhold_from_itunes = True
More details:
@@ -209,7 +219,7 @@ More details:
Shortcut for filling in data
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Instead of assigning those values one at a time, you can assign them all in
one go in the constructor – just like you can with Podcast. Just use the
@@ -225,4 +235,4 @@ See also the example in :doc:`the API Documentation `.
--------------------------------------------------------------------------------
-The final step is :doc:`part_3`
+The final step is :doc:`part_3`.
diff --git a/doc/user/basic_usage_guide/part_3.rst b/doc/user/basic_usage_guide/part_3.rst
index 6596b71..b28cb03 100644
--- a/doc/user/basic_usage_guide/part_3.rst
+++ b/doc/user/basic_usage_guide/part_3.rst
@@ -2,35 +2,40 @@
Generating the RSS
------------------
-Once you've added all the information and all episodes, it's time to
+Once you've added all the information and episodes, you're ready to
take the final step::
- rssfeed = p.rss_str()
+ rssfeed = p.rss_str()
# Print to stdout, just as an example
print(rssfeed)
If you're okay with the default parameters of :meth:`podgen.Podcast.rss_str`,
-you can use a shortcut by converting :class:`~podgen.Podcast` to :obj:`str`::
+you can use a shortcut by converting your :class:`~podgen.Podcast` to :obj:`str`::
rssfeed = str(p)
+ print(rssfeed)
# Or let print convert to str for you
print(p)
-Doing so is the same as calling :meth:`podgen.Podcast.rss_str` with no
-parameters.
-
.. autosummary::
~podgen.Podcast.rss_str
You may also write the feed to a file directly, using :meth:`podgen.Podcast.rss_file`::
- fg.rss_file('rss.xml', minimize=True)
+ p.rss_file('rss.xml', minimize=True)
.. autosummary::
~podgen.Podcast.rss_file
-This concludes the basic usage guide. You might want to look at the
-:doc:`../example` or the :doc:`/api`.
+.. note::
+
+ If there are any mandatory attributes that aren't set, you'll get errors
+ when generating the RSS.
+
+.. note::
+
+ Generating the RSS is not completely free. Save the result to a variable
+ once instead of generating the same RSS over and over.
diff --git a/doc/user/example.rst b/doc/user/example.rst
index 6fd156b..37ee96b 100644
--- a/doc/user/example.rst
+++ b/doc/user/example.rst
@@ -1,14 +1,11 @@
-===============
-Working example
-===============
+============
+Full example
+============
-Below is a working example of how you can go about using PodGen. It
-also shows you how you can use the different properties of Podcast and Episode.
+This example is located at ``podgen/__main__.py`` in the package, and is run
+as part of the :doc:`testing routines `.
.. literalinclude:: ../../podgen/__main__.py
:pyobject: main
:linenos:
-Once you understand the basic way you do things, you're ready to look at the
-:doc:`/api` in conjunction with the :doc:`basic_usage_guide/index` to see exactly what properties you can set, and how they
-affect the end result.
diff --git a/doc/user/fork.rst b/doc/user/fork.rst
index 0e4ee34..b0fc276 100644
--- a/doc/user/fork.rst
+++ b/doc/user/fork.rst
@@ -2,13 +2,15 @@
Why the fork?
=============
-This project is a fork of ``python-feedgen`` which cuts away everything that
+This project is a fork of python-feedgen_ which cuts away everything that
doesn't serve the goal of **making it easy and simple to generate podcasts** from
a Python program. Thus, this project includes only a **subset** of the features
-of ``python-feedgen``. And I don't think anyone in their right mind would accept a pull
+of python-feedgen_. And I don't think anyone in their right mind would accept a pull
request which removes 70% of the features ;-) Among other things, support for ATOM and
Dublin Core is removed, and the remaining code is almost entirely rewritten.
+A more detailed reasoning follows. Read it if you're interested, but feel free
+to skip to :doc:`basic_usage_guide/part_1`.
Inspiration
-----------
@@ -17,21 +19,22 @@ The reason I felt like making such drastic changes, is that the original library
**exceptionally hard to learn** and use. Error messages would not tell you what was wrong,
the concept of extensions is poorly explained and the methods are a bit weird, in that
they function as getters and setters at the same time. The fact that you have three
-separate ways to go about setting multi-value variables, is also a bit confusing.
+separate ways to go about setting multi-value variables is also a bit confusing.
Perhaps the biggest problem, though, is the awkwardness that stems from enabling
RSS and ATOM feeds through the same API. In case you don't know, ATOM is a
-"competitor" to RSS, and has many more capabilities than RSS. However, it is
-not used for podcasting. It is confusing because some methods will map an ATOM value to
+competitor to RSS, and has many more capabilities than RSS. However, it is
+not used for podcasting. The result of mixing both ATOM and RSS include methods that will map an ATOM value to
its closest sibling in RSS, some in logical ways (like the ATOM method ``rights`` setting
the value of the RSS property ``copyright``) and some differ in subtle ways (like using
(ATOM) ``logo`` versus (RSS) ``image``). Other methods are more complex (see ``link``). They're all
confusing, though, since changing one property automatically changes another implicitly.
They also cause bugs, since it is so difficult to wrap your head around how one
interact with another.
-Removing ATOM fixes all these issues.
+Removing ATOM support fixes all these issues.
-Even then, ``python-feedgen`` aims at being comprehensive, which means you must
+Even then, python-feedgen_ aims at being comprehensive, and gives you a one-to-one
+mapping to the resulting XML elements. This means that you must
learn the RSS and podcast standards, which include many legacy elements you
don't really need. For example, the original RSS spec
includes support for an image, but that image is required to be less than 144 pixels
@@ -45,7 +48,7 @@ image must be larger than 1400x1400 pixels, not the history behind everything.
Alignment with the philosophies
-------------------------------
-``python-feedgen``'s code breaks all the philosophies listed above:
+python-feedgen_'s code breaks all the philosophies listed in the :doc:`introduction`:
#. Beautiful is better than ugly, yet all properties are set through hybrid
setter/getter methods.
@@ -56,7 +59,8 @@ Alignment with the philosophies
are available as methods of the extension's name, which suddenly is
available as a property of your FeedGenerator object.
#. Complex is better than complicated, yet an entire framework is built to
- handle extensions, rather than using class inheritance.
+ handle extensions, rather than using class inheritance. (Said framework
+ even requires that the extension resides inside a specific folder!)
#. Readability counts, yet classes are named after their function and not what
they represent, and (again) properties are set through methods.
@@ -70,16 +74,29 @@ bring it there, so it can benefit **everyone**.
Summary of changes
------------------
-* ``FeedGenerator`` is renamed to :class:`~podgen.Podcast` and ``FeedItem`` is accessed
- at ``Podcast.Episode`` (or directly: :class:`~podgen.BaseEpisode`).
-* Support for ATOM removed.
-* Move from using getter and setter methods to using properties, which you can
- assign just like you would assign any other property.
+If you've used python-feedgen_ and want to move over to PodGen, you might as
+well be moving to a completely different library. Everything has been renamed,
+some attributes expect :obj:`bool` where they earlier expected :obj:`str`, and
+so on – you'll have to forget whatever you've learnt about the library.
+Hopefully, the simple API should ease the pain of switching, and make the
+resulting code easier to maintain.
- * Compound values (like managingEditor or enclosure) use
- classes now.
+The following list is not exhaustive.
-* Remove support for some uncommon elements:
+* The module is renamed from ``feedgen`` to ``podgen``.
+* ``FeedGenerator`` is renamed to :class:`~podgen.Podcast` and ``FeedItem`` is
+ renamed to :class:`~podgen.Episode`.
+* All classes are available at package level, so you no longer need to import
+ them from the module they reside in. For example, :class:`podgen.Podcast` and
+ :class:`podgen.Episode`.
+* Support for ATOM is removed.
+* Stop using getter and setter methods and start using attributes.
+
+ * Compound values (like :attr:`~podgen.Podcast.managing_editor` or
+ :attr:`~podgen.Episode.media`) expect
+ objects now, like :class:`~podgen.Person` and :class:`~podgen.Media`.
+
+* Remove support for some uncommon, obsolete or difficult to use elements:
* ttl
* category
@@ -91,6 +108,10 @@ Summary of changes
* Rename the remaining properties so their names don't necessarily match the RSS
elements they map to. Instead, the names should be descriptive and easy to
understand.
+* :attr:`.Podcast.explicit` is now required, and is :obj:`bool`.
* Add shorthand for generating the RSS: Just try to converting your :class:`~podgen.Podcast`
object to :obj:`str`!
-* Improve the documentation
+* Improve the documentation (as you've surely noticed).
+* Move away from the extension framework, and rely on class inheritance instead.
+
+.. _python-feedgen: https://github.com/lkiesow/python-feedgen
diff --git a/doc/user/index.rst b/doc/user/index.rst
deleted file mode 100644
index 9754233..0000000
--- a/doc/user/index.rst
+++ /dev/null
@@ -1,16 +0,0 @@
-==========
-User Guide
-==========
-
-
-New to PodGen? This guide will get you up to speed on how this fork
-came to be, its license as well as how to install and start using it.
-
-.. toctree::
- :maxdepth: 2
-
- introduction
- fork
- installation
- basic_usage_guide/index
- example
diff --git a/doc/user/installation.rst b/doc/user/installation.rst
index 5166b95..b91b64c 100644
--- a/doc/user/installation.rst
+++ b/doc/user/installation.rst
@@ -2,21 +2,11 @@
Installation
============
-#. Clone the `GitHub repository`_.
+Use `pip `_::
-#. Ensure your project has a virtualenv.
+ $ pip install podgen
-#. Activate your project's virtualenv.
+Just a word of warning: PodGen depends on
+`lxml `_, which can take several minutes to build.
-#. Install the requirements listed in ``requirements.txt`` inside podgen::
-
- pip install -r requirements.txt
-
-#. Add this library to the Python path, and you should be able to use it.
-
-
-This is a pretty bad way to install something, but I haven't had the time to
-set up a PyPi package yet. Until then, you'd be better off using the original
-python-feedgen.
-
-.. _GitHub repository: https://github.com/tobinus/python-podgen/tree/podcastgen
+Remember to use a `virtual environment `_!
diff --git a/doc/user/introduction.rst b/doc/user/introduction.rst
index 2b82217..87541fd 100644
--- a/doc/user/introduction.rst
+++ b/doc/user/introduction.rst
@@ -9,10 +9,10 @@ Philosophy
This project is heavily inspired by the wonderful
`Kenneth Reitz `__, known for the
-`Requests `__ library, which features an API which is
+`Requests `__ library, which features an API that is
as beautiful as it is effective. Watching his
`"Documentation is King" talk `__,
-I wanted to make some of the libraries I'm using suitable for use by actual humans.
+I wanted to make some of the libraries I'm using suitable for human consumption too.
This project is to be developed following the same
`PEP 20 `__ idioms as
@@ -30,9 +30,9 @@ To enable this, the project focuses on one task alone: making it easy to generat
Scope
-----
-This library does NOT help you publish a podcast, or manage podcasts. It's just
-a tool that takes information about your podcast, and outputs an RSS feed which
-you can then publish however you want.
+This library does NOT help you publish a podcast, or manage the metadata of your
+podcasts. It's just a tool that accepts information about your podcast and
+outputs an RSS feed which you can then publish however you want.
Both the process of getting information
about your podcast, and publishing it needs to be done by you. Even then,
@@ -45,12 +45,15 @@ PodGen is geared towards developers who aren't super familiar with
RSS and XML. If you know exactly how you want the XML to look, then you're
better off using a template engine like Jinja2 (even if friends don't let
friends touch XML bare-handed). If you just want an easy way to create and
-manage your podcasts, use `Podcast Generator `.
+manage your podcasts, use `Podcast Generator `_.
-------
License
-------
PodGen is licensed under the terms of both the FreeBSD license and the LGPLv3+.
Choose the one which is more convenient for you. For more details, have a look
-at license.bsd and license.lgpl.
+at license.bsd_ and license.lgpl_.
+
+.. _license.bsd: https://github.com/tobinus/python-podgen/blob/master/license.bsd
+.. _license.lgpl: https://github.com/tobinus/python-podgen/blob/master/license.lgpl
diff --git a/podgen/category.py b/podgen/category.py
index 6b8b398..2d18b32 100644
--- a/podgen/category.py
+++ b/podgen/category.py
@@ -12,7 +12,7 @@ class Category(object):
Example::
- >>> from podgen.category import Category
+ >>> from podgen import Category
>>> c = Category("Music")
>>> c.category
Music
@@ -95,13 +95,19 @@ def __init__(self, category, subcategory=None):
@property
def category(self):
- """The category represented by this object. Read-only."""
+ """The category represented by this object. Read-only.
+
+ :type: :obj:`str`
+ """
return self.__category
# Make this attribute read-only by not implementing setter
@property
def subcategory(self):
- """The subcategory this object represents. Read-only."""
+ """The subcategory this object represents. Read-only.
+
+ :type: :obj:`str`
+ """
return self.__subcategory
# Make this attribute read-only by not implementing setter
diff --git a/podgen/episode.py b/podgen/episode.py
index 17c6ac1..baeede5 100644
--- a/podgen/episode.py
+++ b/podgen/episode.py
@@ -40,7 +40,7 @@ class Episode(object):
ValueError if you set an attribute to an invalid value.
You must have filled in either :attr:`.title` or :attr:`.summary` before
- the RSS is generated.
+ the RSS can be generated.
To add an episode to a podcast::
@@ -51,12 +51,12 @@ class Episode(object):
You may also replace the last two lines with a shortcut::
- >>> episode = p.add_episode(Episode())
+ >>> episode = p.add_episode(podgen.Episode())
.. seealso::
- The :doc:`Basic Usage Guide `
+ :doc:`/user/basic_usage_guide/part_2`
A friendlier introduction to episodes.
"""
@@ -79,19 +79,24 @@ def __init__(self, **kwargs):
up to 4000 characters in length.
See also :py:attr:`.Episode.subtitle` and
- :py:attr:`.Episode.long_summary`."""
+ :py:attr:`.Episode.long_summary`.
+
+ :type: :obj:`str` which can be parsed as XHTML.
+ :RSS: description"""
self.long_summary = None
"""A long (read: full) summary, which supplements the shorter
- :attr:`~podgen.Episode.summary`.
+ :attr:`~podgen.Episode.summary`. Like summary, this must be compatible
+ with XHTML parsers; use :func:`podgen.htmlencode` if this isn't HTML.
This attribute should be seen as a full, longer variation of
summary if summary exists. Even then, the long_summary should be
independent from summary, in that you only need to read one of them.
This means you may have to repeat the first sentences.
- If summary does not exist but this does, this is used in place of
- summary."""
+ :type: :obj:`str` which can be parsed as XHTML.
+ :RSS: content:encoded or description
+ """
self.__media = None
@@ -101,7 +106,7 @@ def __init__(self, **kwargs):
If not present, the URL of the enclosed media is used. This is usually
the best way to go, **as long as the media URL doesn't change**.
- Set the id to boolean False if you don't want to associate any id to
+ Set the id to boolean ``False`` if you don't want to associate any id to
this episode.
It is important that an episode keeps the same ID until the end of time,
@@ -116,17 +121,28 @@ def __init__(self, **kwargs):
domain which you own (for example, use something like
http://example.org/podcast/episode1 if you own example.org).
- This property corresponds to the RSS GUID element."""
+ :type: :obj:`str`, :obj:`None` to use default or :obj:`False` to leave
+ out.
+ :RSS: guid
+ """
self.link = None
- """The link to the full version of this episode description.
- Remember to start the link with the scheme, e.g. https://."""
+ """The link to the full version of this episode's :attr:`.summary`.
+ Remember to start the link with the scheme, e.g. https://.
+
+ :type: :obj:`str`
+ :RSS: link
+ """
self.__publication_date = None
self.title = None
"""This episode's human-readable title.
- Title is mandatory and should not be blank."""
+ Title is mandatory and should not be blank.
+
+ :type: :obj:`str`
+ :RSS: title
+ """
# ITunes tags
# http://www.apple.com/itunes/podcasts/specs.html#rss
@@ -138,12 +154,15 @@ def __init__(self, **kwargs):
self.__explicit = None
- self.is_closed_captioned = None
- """Whether this podcast includes a video episode with embedded closed
- captioning support.
+ self.is_closed_captioned = False
+ """Whether this podcast includes a video episode with embedded `closed
+ captioning`_ support. Defaults to ``False``.
- The two values for this tag are ``True`` and
- ``False``."""
+ :type: :obj:`bool`
+ :RSS: itunes:isClosedCaptioned
+
+ .. _closed captioning: https://en.wikipedia.org/wiki/Closed_captioning
+ """
self.__position = None
@@ -151,7 +170,11 @@ def __init__(self, **kwargs):
"""A short subtitle.
This is shown in the Description column in iTunes.
- The subtitle displays best if it is only a few words long."""
+ The subtitle displays best if it is only a few words long.
+
+ :type: :obj:`str`
+ :RSS: itunes:subtitle
+ """
# It is time to assign the keyword arguments
for attribute, value in iteritems(kwargs):
@@ -162,7 +185,13 @@ def __init__(self, **kwargs):
"recognized!" % (attribute, value))
def rss_entry(self):
- """Create a RSS item and return it."""
+ """Create an RSS item using lxml's etree and return it.
+
+ This is primarily used by :class:`podgen.Podcast` when generating the
+ podcast's RSS feed.
+
+ :returns: etree.Element('item')
+ """
ITUNES_NS = 'http://www.itunes.com/dtds/podcast-1.0.dtd'
DUBLIN_NS = 'http://purl.org/dc/elements/1.1/'
@@ -279,8 +308,8 @@ def authors(self):
"""List of :class:`~podgen.Person` that contributed to this
episode.
- The authors don't need to have both name and email set. The names are
- shown under the podcast's title on iTunes.
+ The authors don't need to have both name and email set. They're usually
+ not displayed anywhere.
.. note::
@@ -308,6 +337,9 @@ def authors(self):
>>> # Or assign a new list (discarding earlier authors)
>>> ep.authors = [Person("John Doe", "johndoe@example.org"),
... Person("Mary Sue", "marysue@example.org")]
+
+ :type: :obj:`list` of :class:`podgen.Person`
+ :RSS: author or dc:creator, and itunes:author
"""
return self.__authors
@@ -322,11 +354,22 @@ def authors(self, authors):
@property
def publication_date(self):
- """Set or get the time that this episode first was made public.
+ """The time and date this episode was first published.
+
+ The value can be a :obj:`str`, which will be parsed and
+ made into a :class:`datetime.datetime` object when assigned. You may
+ also assign a :class:`datetime.datetime` object directly. In both cases,
+ you must ensure that the value includes timezone information.
+
+ :type: :obj:`str` (will be converted to and stored as
+ :class:`datetime.datetime`) or :class:`datetime.datetime`.
+ :RSS: pubDate
+
+ .. note::
- The value can either be a string which will automatically be parsed or a
- datetime.datetime object. In both cases you must ensure that the value
- includes timezone information.
+ Don't use the media file's modification date as the publication
+ date, unless they're the same. It looks very odd when an episode
+ suddenly pops up in the feed, but it claims to be several hours old!
"""
return self.__publication_date
@@ -348,13 +391,16 @@ def media(self):
"""Get or set the :class:`~podgen.Media` object that is attached
to this episode.
- Note that if :py:attr:`.id` is not set, the enclosure's url is used as
- the globally unique identifier. If you rely on this, you should make
- sure the url never changes, since changing the id messes up with clients
- (they will think this episode is new again, even if the user already
- has listened to it). Therefore, you should only rely on this behaviour
- if you own the domain which the episodes reside on. If you don't, then
- you must set :py:attr:`.id` to an appropriate value manually.
+ Note that if :py:attr:`.id` is not set, the media's URL is used as
+ the id. If you rely on this, you should make sure the URL never changes,
+ since changing the id messes up with clients (they will think this
+ episode is new again, even if the user has listened to it already).
+ Therefore, you should only rely on this behaviour if you own the domain
+ which the episodes reside on. If you don't, then you must set
+ :py:attr:`.id` to an appropriate value manually.
+
+ :type: :class:`podgen.Media`
+ :RSS: enclosure and itunes:duration
"""
return self.__media
@@ -374,17 +420,20 @@ def media(self, media):
@property
def withhold_from_itunes(self):
- """Get or set the iTunes block attribute. Use this to prevent episodes
- from appearing in the iTunes podcast directory. Note that the episode
- can still be found by inspecting the XML, so it is still public.
+ """Prevent this episode from appearing in the iTunes podcast directory.
+ Note that the episode can still be found by inspecting the XML, so it is
+ still public.
- One use case is if you know that this episode will get you kicked
+ One use case would be if you knew that this episode would get you kicked
out from iTunes, should it make it there. In such cases, you can set
withhold_from_itunes to ``True`` so this episode isn't published on
iTunes, allowing you to publish it to everyone else while keeping your
podcast on iTunes.
This attribute defaults to ``False``, of course.
+
+ :type: :obj:`bool`
+ :RSS: itunes:block
"""
return self.__withhold_from_itunes
@@ -401,29 +450,35 @@ def withhold_from_itunes(self, withhold_from_itunes):
@property
def image(self):
- """The podcast episode's image.
+ """The podcast episode's image, overriding the podcast's
+ :attr:`~.Podcast.image`.
+
+ This attribute specifies the absolute URL to the artwork for your
+ podcast. iTunes prefers square images that are at least ``1400x1400``
+ pixels.
+
+ iTunes supports images in JPEG and PNG formats with an RGB color space
+ (CMYK is not supported). The URL must end in ".jpg" or ".png".
+
+ :type: :obj:`str`
+ :RSS: itunes:image
+
+ .. note::
+
+ If you change an episode’s image, you should also change the file’s
+ name; iTunes doesn't check the actual file to see if it's changed.
+
+ Additionally, the server hosting your cover art image must allow HTTP
+ HEAD requests.
.. warning::
Almost no podcatchers support this. iTunes supports it only if you
embed the cover in the media file (the same way you would embed
- an album cover), and recommends you use Garageband's Enhanced
- Podcast feature. If you don't, the podcast's image is used instead.
+ an album cover), and recommends that you use Garageband's Enhanced
+ Podcast feature.
- This tag specifies the artwork for your podcast.
- iTunes prefers square .jpg images that are at least 1400x1400 pixels,
- which is different from what is specified for the standard RSS image
- tag. In order for a podcast to be eligible for an iTunes Store feature,
- the accompanying image must be at least 1400x1400 pixels.
-
- iTunes supports images in JPEG and PNG formats with an RGB color space
- (CMYK is not supported). The URL must end in ".jpg" or ".png".
-
- If you change an episode’s image, you should also change the file’s
- name. iTunes may not change the image if it checks your feed and the
- image URL is the same. The server hosting your cover art image must
- allow HTTP head requests for iTunes to be able to automatically update
- your cover art.
+ The podcast's image is used if this isn't supported.
"""
return self.__image
@@ -444,13 +499,16 @@ def explicit(self):
inappropriate for children.
The value of the podcast's explicit attribute is used by default, if
- this is ``None``.
+ this is kept as ``None``.
If you set this to ``True``, an "explicit" parental advisory
graphic will appear in the Name column in iTunes. If the value is
``False``, the parental advisory type is considered Clean, meaning that
no explicit language or adult content is included anywhere in this
episode, and a "clean" graphic will appear.
+
+ :type: :obj:`bool`
+ :RSS: itunes:explicit
"""
return self.__explicit
@@ -471,9 +529,14 @@ def position(self):
If you would like this episode to appear first, set it to ``1``.
If you want it second, set it to ``2``, and so on. If multiple episodes
- share the same position, they will be sorted by their publication date.
+ share the same position, they will be sorted by their
+ :attr:`publication date <.Episode.publication_date>`.
+
+ To remove the order from the episode, set the position back to
+ :obj:`None`.
- To remove the order from the episode, set the position back to ``None``.
+ :type: :obj:`int`
+ :RSS: itunes:order
"""
return self.__position
diff --git a/podgen/media.py b/podgen/media.py
index e5fdb06..558bc2f 100644
--- a/podgen/media.py
+++ b/podgen/media.py
@@ -12,12 +12,15 @@ class Media(object):
A media file can be a sound file (most typical), video file or a document.
- You should provide the url at which this media can be found, and the media's
- file size in bytes. Optionally, you can provide the type of media
- (expressed in the media type format). When not given in the constructor, it
- will be found automatically by looking at the url's file extension. If the
- url's file extension isn't supported by iTunes, you will get an error if you
- don't supply the type.
+ You should provide the absolute URL at which this media can be found, and
+ the media's file size in bytes.
+
+ Optionally, you can provide the type of media (expressed using MIME types).
+ When not given in the constructor, it will be found automatically by looking
+ at the url's file extension. If the url's file extension isn't supported by
+ iTunes, you will get an error if you don't supply the type.
+
+ You are also highly encouraged to provide the duration of the media.
.. note::
@@ -27,7 +30,7 @@ class Media(object):
.. note::
- A warning called :class:`~podgen.not_supported_by_itunes_warning.NotSupportedByItunesWarning`
+ A warning called :class:`~podgen.NotSupportedByItunesWarning`
will be issued if your URL or type isn't compatible with iTunes. See
the Python documentation for more details on :mod:`warnings`.
@@ -77,7 +80,10 @@ def url(self):
Only absolute URLs are allowed, so make sure it starts with http:// or
https://. The server should support HEAD-requests and byte-range
- requests."""
+ requests.
+
+ :type: :obj:`str`
+ """
return self._url
@url.setter
@@ -99,18 +105,21 @@ def url(self, url):
def size(self):
"""The media's file size in bytes.
- You can either provide the number of bytes as an int, or you can
- provide a human-readable str with a unit, like MB or GiB.
+ You can either provide the number of bytes as an :obj:`int`, or you can
+ provide a human-readable :obj:`str` with a unit, like MB or GiB.
An unknown size is represented as 0. This should ONLY be used in
exceptional cases, where it is theoretically impossible to determine
the file size (for example if it's a stream). Setting the size to 0
will issue a UserWarning.
+ :type: :obj:`str` (which will be converted to and stored as :obj:`int`)
+ or :obj:`int`
+
.. note::
If you provide a string, it will be translated to int when the
- assignment happens. Thus, on subsequent access, you will get the
+ assignment happens. Thus, on subsequent accesses, you will get the
resulting int, not the string you put in.
.. note::
@@ -168,14 +177,16 @@ def type(self):
See https://en.wikipedia.org/wiki/Media_type for an introduction.
+ :type: :obj:`str`
+
.. note::
If you leave out type when creating a new Media object, the
- type will be auto-detected from the :attr:`~podgen.media.Media.url`
+ type will be auto-detected from the :attr:`~podgen.Media.url`
attribute. However, this won't happen automatically other than
during initialization. If you want to autodetect type when
assigning a new value to url, you should use
- :meth:`~podgen.media.Media.get_type`.
+ :meth:`~podgen.Media.get_type`.
"""
return self._type
@@ -192,11 +203,15 @@ def type(self, type):
self._type = type
def get_type(self, url):
- """Autodetect the media type, given the url.
+ """Guess the MIME type from the URL.
+
+ This is used to fill in :attr:`~.Media.type` when it is not given (and
+ thus called implicitly by the constructor), but you can call it
+ yourself.
Example::
- >>> from podgen.media import Media
+ >>> from podgen import Media
>>> m = Media("http://example.org/1.mp3", 136532744)
>>> # The type was detected from the url:
>>> m.type
@@ -210,6 +225,11 @@ def get_type(self, url):
>>> m.type = m.get_type(m.url)
>>> m.type
audio/x-m4a
+
+ :param url: The URL which should be used to guess the MIME type.
+ :type url: str
+ :returns: The guessed MIME type.
+ :raises: ValueError if the MIME type couldn't be guessed from the URL.
"""
file_extension = urlparse(url).path.split(".")[-1]
try:
@@ -227,9 +247,8 @@ def duration(self):
:type: :class:`datetime.timedelta`
:raises: :obj:`TypeError` if you try to assign anything other than
- :class:`datetime.timedelta` instances or None to this attribute.
- Raises :obj:`ValueError` if a negative timedelta value is
- given.
+ :class:`datetime.timedelta` or :obj:`None` to this attribute. Raises
+ :obj:`ValueError` if a negative timedelta value is given.
"""
return self._duration
@@ -252,7 +271,10 @@ def duration_str(self):
This is just an alternate, read-only view of :attr:`.duration`.
- If :attr:`.duration` is :obj:`None`, this will be :obj:`None` as well.
+ If :attr:`.duration` is :obj:`None`, then this will be :obj:`None` as
+ well.
+
+ :type: :obj:`str`
"""
if self.duration is None:
return None
@@ -279,7 +301,7 @@ def create_from_server_response(cls, requests, url, size=None, type=None,
Example (assuming the server responds with Content-Length: 252345991 and
Content-Type: audio/mpeg)::
- >>> from podgen.media import Media
+ >>> from podgen import Media
>>> import requests # from requests package
>>> # Assume an episode is hosted at example.com
>>> m = Media.create_from_server_response(requests,
@@ -302,7 +324,7 @@ def create_from_server_response(cls, requests, url, size=None, type=None,
:type type: str or None
:param duration: The media's duration.
:type duration: :class:`datetime.timedelta` or :obj:`None`.
- :returns: New instance of Media with all fields filled in.
+ :returns: New instance of Media with url, size and type filled in.
:raises: The appropriate requests exceptions are thrown when networking
errors occur. RuntimeError is thrown if some information isn't
given and isn't found in the server's response."""
diff --git a/podgen/person.py b/podgen/person.py
index 3a93f2f..9100ec6 100644
--- a/podgen/person.py
+++ b/podgen/person.py
@@ -1,8 +1,8 @@
class Person(object):
"""Data-oriented class representing a single person or entity.
- A Person can represent both real persons and organizations, entities
- and so on. Example::
+ A Person can represent both real persons and less personal entities like
+ organizations. Example::
>>> p.authors = [Person("Example Radio", "mail@example.org")]
@@ -13,8 +13,8 @@ class Person(object):
.. warning::
- **Any names and email addresses** you put into a Person object, will
- eventually be included in the feed and thus **published** together with
+ **Any names and email addresses** you put into a Person object will
+ eventually be included and **published** together with
the feed. If you want to keep a name or email address private, then you
must make sure it isn't used in a Person object (or to be precise: that
the Person object with the name or email address isn't used in any
@@ -58,7 +58,10 @@ def _is_valid(self, name, email):
@property
def name(self):
- """This person's name."""
+ """This person's name.
+
+ :type: :obj:`str`
+ """
return self.__name
@name.setter
@@ -71,7 +74,10 @@ def name(self, new_name):
@property
def email(self):
- """This person's public email address."""
+ """This person's public email address.
+
+ :type: :obj:`str`
+ """
return self.__email
@email.setter
diff --git a/podgen/podcast.py b/podgen/podcast.py
index 7a1b737..87c1c79 100644
--- a/podgen/podcast.py
+++ b/podgen/podcast.py
@@ -36,10 +36,15 @@ class Podcast(object):
* :attr:`~podgen.Podcast.description`
* :attr:`~podgen.Podcast.explicit`
+ All attributes can be assigned :obj:`None` in addition to the types
+ specified below. Types etc. are checked during assignment, to help you
+ discover errors earlier. Duck typing is employed wherever a class in podgen
+ is expected.
+
There is a **shortcut** you can use when creating new Podcast objects, that
lets you populate the attributes using the constructor. Use keyword
arguments with the **attribute name as keyword** and the desired value as
- value::
+ value. As an example::
>>> import podgen
>>> # The following...
@@ -71,26 +76,31 @@ def __init__(self, **kwargs):
# http://www.rssboard.org/rss-specification
# Mandatory:
self.name = None
- """The name of the podcast. It should be a human
- readable title. Often the same as the title of the
- associated website. This is mandatory for RSS and must
- not be blank.
+ """The name of the podcast as a :obj:`str`. It should be a human
+ readable title. Often the same as the title of the associated website.
+ This is mandatory and must not be blank.
- This will set rss:title.
+ :type: :obj:`str`
+ :RSS: title
"""
self.website = None
- """This podcast's website's absolute URL.
+ """The absolute URL of this podcast's website.
- One of the mandatory attributes.
+ This is one of the mandatory attributes.
- This corresponds to the RSS link element.
+ :type: :obj:`str`
+ :RSS: link
"""
self.description = None
- """The description of the feed, which is a phrase or sentence describing
- the channel. It is mandatory for RSS feeds, and is shown under the
- podcast's name on the iTunes store page."""
+ """The description of the podcast, which is a phrase or sentence
+ describing it to potential new subscribers. It is mandatory for RSS
+ feeds, and is shown under the podcast's name on the iTunes store page.
+
+ :type: :obj:`str`
+ :RSS: description
+ """
self.explicit = None
"""Whether this podcast may be inappropriate for children or not.
@@ -104,7 +114,11 @@ def __init__(self, **kwargs):
in the Name column in iTunes. If it is set to ``False``,
the parental advisory type is considered Clean, meaning that no explicit
language or adult content is included anywhere in the episodes, and a
- "clean" graphic will appear."""
+ "clean" graphic will appear.
+
+ :type: :obj:`bool`
+ :RSS: itunes:explicit
+ """
# Optional:
self.__cloud = None
@@ -120,13 +134,19 @@ def __init__(self, **kwargs):
you do not need a copyright statement for something to be protected by
copyright. If you intend to put the podcast in public domain or license
it under a Creative Commons license, you should say so in the copyright
- notice."""
+ notice.
+
+ :type: :obj:`str`
+ :RSS: copyright"""
self.__docs = 'http://www.rssboard.org/rss-specification'
self.generator = self._feedgen_generator_str
"""A string identifying the software that generated this RSS feed.
- Defaults to a string identifying PodcastGenerator.
+ Defaults to a string identifying PodGen.
+
+ :type: :obj:`str`
+ :RSS: generator
.. seealso::
@@ -144,7 +164,11 @@ def __init__(self, **kwargs):
It must be a two-letter code, as found in ISO639-1, with the
possibility of specifying subcodes (eg. en-US for American English).
See http://www.rssboard.org/rss-language-codes and
- http://www.loc.gov/standards/iso639-2/php/code_list.php"""
+ http://www.loc.gov/standards/iso639-2/php/code_list.php
+
+ :type: :obj:`str`
+ :RSS: language
+ """
self.__last_updated = None
@@ -170,14 +194,16 @@ def __init__(self, **kwargs):
the iTunes catalogue to implement the search feature. Listeners will
still be able to subscribe by adding the feed's address manually.
- If you don't intend on submitting this podcast to iTunes, you can set
- this to True as a way of showing iTunes the middle finger (and prevent
- others from submitting it as well).
+ If you don't intend to submit this podcast to iTunes, you can set
+ this to ``True`` as a way of giving iTunes the middle finger, and
+ perhaps more importantly, preventing others from submitting it as well.
Set it to ``True`` to withhold the entire podcast from iTunes. It is set
to ``False`` by default, of course.
- :type: bool"""
+ :type: :obj:`bool`
+ :RSS: itunes:block
+ """
self.__category = None
@@ -188,10 +214,13 @@ def __init__(self, **kwargs):
self.new_feed_url = None
"""When set, tell iTunes that your feed has moved to this URL.
- After adding the tag to your old feed, you should maintain the old feed
+ After adding this attribute, you should maintain the old feed
for 48 hours before retiring it. At that point, iTunes will have updated
the directory with the new feed URL.
+ :type: :obj:`str`
+ :RSS: itunes:new-feed-url
+
.. warning::
iTunes supports this mechanic of changing your feed's location.
@@ -203,7 +232,7 @@ def __init__(self, **kwargs):
.. warning::
- Make sure the new URL here is correct, or else you're making
+ Make sure the new URL you set is correct, or else you're making
people switch to a URL that doesn't work!
"""
@@ -212,7 +241,11 @@ def __init__(self, **kwargs):
self.subtitle = None
"""The subtitle for your podcast, shown mainly as a very short
description on iTunes. The subtitle displays best if it is only a few
- words long, like a short slogan."""
+ words long, like a short slogan.
+
+ :type: :obj:`str`
+ :RSS: itunes:subtitle
+ """
# Populate the podcast with the keyword arguments
for attribute, value in iteritems(kwargs):
@@ -226,10 +259,13 @@ def __init__(self, **kwargs):
@property
def episodes(self):
- """List of episodes that are part of this podcast.
+ """List of :class:`.Episode` objects that are part of this podcast.
See :py:meth:`.add_episode` for an easy way to create new episodes and
assign them to this podcast in one call.
+
+ :type: :obj:`list` of :class:`podgen.Episode`
+ :RSS: item elements
"""
return self.__episodes
@@ -244,32 +280,39 @@ def episode_class(self):
"""Class used to represent episodes.
This is used by :py:meth:`.add_episode` when creating new episode
- objects, and you may use it too when creating episodes.
+ objects, and you, too, may use it when creating episodes.
- By default, this property points to :py:class:`Episode`.
+ By default, this property points to :py:class:`.Episode`.
- When assigning a new class to ``episode_class``, you must make sure the
- new value (1) is a class and not an instance, and (2) is a subclass of
- Episode (or is Episode itself).
+ When assigning a new class to ``episode_class``, you must make sure that
+ the new value (1) is a class and not an instance, and (2) that it is a
+ subclass of Episode (or is Episode itself).
Example of use::
>>> # Create new podcast
>>> from podgen import Podcast, Episode
>>> p = Podcast()
+
>>> # Normal way of creating new episodes
>>> episode1 = Episode()
>>> p.episodes.append(episode1)
+
>>> # Or use add_episode (and thus episode_class indirectly)
>>> episode2 = p.add_episode()
+
>>> # Or use episode_class directly
>>> episode3 = p.episode_class()
>>> p.episodes.append(episode3)
+
>>> # Say you want to use AlternateEpisode class instead of Episode
>>> from mymodule import AlternateEpisode
>>> p.episode_class = AlternateEpisode
+
>>> episode4 = p.add_episode()
>>> episode4.title("This is an instance of AlternateEpisode!")
+
+ :type: :obj:`class` which extends :class:`podgen.Episode`
"""
return self.__episode_class
@@ -292,20 +335,18 @@ def add_episode(self, new_episode=None):
object if it's not provided, and returns it. This
is the easiest way to add episodes to a podcast.
- :param new_episode: Episode object to add. A new instance of
- self.Episode is used if new_episode is omitted.
+ :param new_episode: :class:`.Episode` object to add. A new instance of
+ :attr:`.episode_class` is used if ``new_episode`` is omitted.
:returns: Episode object created or passed to this function.
Example::
...
- >>> entry = feedgen.add_episode()
- >>> entry.title('First feed entry')
- 'First feed entry'
+ >>> episode1 = p.add_episode()
+ >>> episode1.title = 'First episode'
>>> # You may also provide an episode object yourself:
- >>> another_entry = feedgen.add_episode(podgen.Episode())
- >>> another_entry.title('My second feed entry')
- 'My second feed entry'
+ >>> another_episode = p.add_episode(podgen.Episode())
+ >>> another_episode.title = 'My second episode'
Internally, this method creates a new instance of
:attr:`~podgen.Episode.episode_class`, which means you can change what
@@ -420,11 +461,15 @@ def _create_rss(self):
pubDate.text = formatRFC2822(actual_pubDate)
if self.skip_hours:
+ # Ensure any modifications to the set are accounted for
+ self.skip_hours = self.skip_hours
skipHours = etree.SubElement(channel, 'skipHours')
for h in self.skip_hours:
hour = etree.SubElement(skipHours, 'hour')
hour.text = str(h)
if self.skip_days:
+ # Ensure any modifications to the set are accounted for
+ self.skip_days = self.skip_days
skipDays = etree.SubElement(channel, 'skipDays')
for d in self.skip_days:
day = etree.SubElement(skipDays, 'day')
@@ -492,18 +537,18 @@ def __str__(self):
def rss_str(self, minimize=False, encoding='UTF-8',
xml_declaration=True):
- """Generates an RSS feed and returns the feed XML as string.
+ """Generate an RSS feed and return the feed XML as string.
:param minimize: Set to True to disable splitting the feed into multiple
lines and adding properly indentation, saving bytes at the cost of
- readability.
+ readability (default: False).
:type minimize: bool
:param encoding: Encoding used in the XML file (default: UTF-8).
:type encoding: str
- :param xml_declaration: If an XML declaration should be added to the
- output (Default: enabled).
+ :param xml_declaration: Whether an XML declaration should be added to
+ the output (default: True).
:type xml_declaration: bool
- :returns: String representation of the RSS feed.
+ :returns: The generated RSS feed as a :obj:`str`.
"""
feed = self._create_rss()
return etree.tostring(feed, pretty_print=not minimize, encoding=encoding,
@@ -512,19 +557,20 @@ def rss_str(self, minimize=False, encoding='UTF-8',
def rss_file(self, filename, minimize=False,
encoding='UTF-8', xml_declaration=True):
- """Generates an RSS feed and write the resulting XML to a file.
+ """Generate an RSS feed and write the resulting XML to a file.
:param filename: Name of file to write, or a file-like object, or a URL.
:type filename: str or fd
:param minimize: Set to True to disable splitting the feed into multiple
lines and adding properly indentation, saving bytes at the cost of
- readability.
+ readability (default: False).
:type minimize: bool
- :param encoding: Encoding used in the XML file (default: UTF-8).
+ :param encoding: Encoding used in the XML file (default: UTF-8).
:type encoding: str
- :param xml_declaration: If an XML declaration should be added to the
- output (Default: enabled).
+ :param xml_declaration: Whether an XML declaration should be added to
+ the output (default: True).
:type xml_declaration: bool
+ :returns: Nothing.
"""
feed = self._create_rss()
doc = etree.ElementTree(feed)
@@ -533,20 +579,22 @@ def rss_file(self, filename, minimize=False,
@property
def last_updated(self):
- """The last time the feed was modified in a significant way. Most often,
- it is taken to mean the last time the feed was generated, which is why
- it defaults to the time and date at which the RSS is generated, if set
- to None. The default should be sufficient for most, if not all, use
- cases.
-
- The value can either be a string which will automatically be parsed or a
- datetime.datetime object. In any case it is necessary that the value
- include timezone information.
-
- This corresponds to rss:lastBuildDate. Set this to False to have no
- lastBuildDate element in the feed (and thus suppress the default).
-
- :type: :obj:`str`, :class:`datetime.datetime` or :obj:`None`.
+ """The last time the feed was generated. It defaults to the time and
+ date at which the RSS is generated, if set to :obj:`None`. The default
+ should be sufficient for most, if not all, use cases.
+
+ The value can either be a string, which will automatically be parsed
+ into a :class:`datetime.datetime` object when assigned, or a
+ :class:`datetime.datetime` object. In any case, the time and date must
+ be timezone aware.
+
+ Set this to ``False`` to leave out this element instead of using the
+ default.
+
+ :type: :class:`datetime.datetime`, :obj:`str` (will be converted to
+ and stored as :class:`datetime.datetime`), :obj:`None` for default or
+ :obj:`False` to leave out.
+ :RSS: lastBuildDate
"""
return self.__last_updated
@@ -566,11 +614,11 @@ def last_updated(self, last_updated):
@property
def cloud(self):
"""The cloud data of the feed, as a 5-tuple. It specifies a web service
- that supports the rssCloud interface which can be implemented in
- HTTP-POST, XML-RPC or SOAP 1.1.
+ that supports the (somewhat dated) rssCloud interface, which can be
+ implemented in HTTP-POST, XML-RPC or SOAP 1.1.
- The tuple should look like this: ``(domain, port, path, registerProcedure,
- protocol)``.
+ The tuple should look like this: ``(domain, port, path,
+ registerProcedure, protocol)``.
:domain: The domain where the webservice can be found.
:port: The port the webservice listens to.
@@ -583,6 +631,10 @@ def cloud(self):
p.cloud = ("podcast.example.org", 80, "/rpc", "cloud.notify",
"xml-rpc")
+ :type: :obj:`tuple` with (:obj:`str`, :obj:`int`, :obj:`str`,
+ :obj:`str`, :obj:`str`)
+ :RSS: cloud
+
.. tip::
PubSubHubbub is a competitor to rssCloud, and is the preferred
@@ -610,15 +662,19 @@ def cloud(self, cloud):
self.__cloud = None
def set_generator(self, generator=None, version=None, uri=None,
- exclude_feedgen=False):
+ exclude_podgen=False):
"""Set the generator of the feed, formatted nicely, which identifies the
- software used to generate the feed, for debugging and other purposes.
+ software used to generate the feed.
:param generator: Software used to create the feed.
+ :type generator: str
:param version: (Optional) Version of the software, as a tuple.
- :param uri: (Optional) URI the software can be found.
- :param exclude_feedgen: (Optional) Set to True to disable the mentioning
- of the python-podgen library.
+ :type version: :obj:`tuple` of :obj:`int`
+ :param uri: (Optional) The software's website.
+ :type uri: str
+ :param exclude_podgen: (Optional) Set to True if you don't want
+ PodGen to be mentioned (e.g., "My Program (using PodGen 1.0.0)")
+ :type exclude_podgen: bool
.. seealso::
@@ -628,7 +684,7 @@ def set_generator(self, generator=None, version=None, uri=None,
"""
self.generator = self._program_name_to_str(generator, version, uri) + \
(" (using %s)" % self._feedgen_generator_str
- if not exclude_feedgen else "")
+ if not exclude_podgen else "")
def _program_name_to_str(self, generator=None, version=None, uri=None):
return generator + \
@@ -673,6 +729,9 @@ def authors(self):
>>> # Or they can be given as new list (overriding earlier authors)
>>> p.authors = [Person("John Doe", "johndoe@example.org"),
... Person("Mary Sue", "marysue@example.org")]
+
+ :type: :obj:`list` of :class:`podgen.Person`
+ :RSS: managingEditor or dc:creator, and itunes:author
"""
return self.__authors
@@ -687,21 +746,26 @@ def authors(self, authors):
@property
def publication_date(self):
- """Set or get the publication date for the content in the channel. For
- example, the New York Times publishes on a daily basis, the publication
- date flips once every 24 hours. That's when the publication date of the
- channel changes.
-
- :type: None, a string which will automatically be parsed or a
- datetime.datetime object. In any case it is necessary that the value
- include timezone information.
- :Default value: If this is None when the feed is generated, the
- publication date of the episode with the latest publication date (which
- may be in the future) is used. If there are no episodes, the publication
- date is omitted from the feed.
+ """The publication date for the content in this podcast. You
+ probably want to use the default value.
+
+ :Default value: If this is :obj:`None` when the feed is generated, the
+ publication date of the episode with the latest publication date
+ (which may be in the future) is used. If there are no episodes, the
+ publication date is omitted from the feed.
+
+ If you set this to a :obj:`str`, it will be parsed and made into a
+ :class:`datetime.datetime` object when assigned. You may also set it to
+ a :class:`datetime.datetime` object directly. In any case, the time and
+ date must be timezone aware.
If you want to forcefully omit the publication date from the feed, set
this to ``False``.
+
+ :type: :class:`datetime.datetime`, :obj:`str` (will be converted to
+ and stored as :class:`datetime.datetime`), :obj:`None` for default or
+ :obj:`False` to leave out.
+ :RSS: pubDate
"""
return self.__publication_date
@@ -718,11 +782,17 @@ def publication_date(self, publication_date):
@property
def skip_hours(self):
- """Set of hours in which feed readers don't need to refresh this feed.
+ """Set of hours of the day in which podcatchers don't need to refresh
+ this feed.
+
+ This isn't widely supported by podcatchers.
The hours are represented as integer values from 0 to 23.
+ Note that while the content of the set is checked when it is first
+ assigned to ``skip_hours``, further changes to the set "in place" will
+ not be checked before you generate the RSS.
- For example, to skip hours between 18 and 7::
+ For example, to stop refreshing the feed between 18 and 7::
>>> from podgen import Podcast
>>> p = Podcast()
@@ -732,6 +802,9 @@ def skip_hours(self):
>>> p.skip_hours |= set(range(8))
>>> p.skip_hours
{0, 1, 2, 3, 4, 5, 6, 7, 18, 19, 20, 21, 22, 23}
+
+ :type: :obj:`set` of :obj:`int`
+ :RSS: skipHours
"""
return self.__skip_hours
@@ -749,17 +822,24 @@ def skip_hours(self, hours):
def skip_days(self):
"""Set of days in which podcatchers don't need to refresh this feed.
- The days are represented using strings of their dayname, like "Monday"
- or "wednesday".
+ This isn't widely supported by podcatchers.
+
+ The days are represented using strings of their English names, like
+ "Monday" or "wednesday". The day names are automatically capitalized
+ when the set is assigned to ``skip_days``, but subsequent changes to the
+ set "in place" are only checked and capitalized when the RSS feed is
+ generated.
- For example, to skip the weekend::
+ For example, to stop refreshing the feed in the weekend::
>>> from podgen import Podcast
>>> p = Podcast()
- >>> p.skip_days = {"Friday", "Saturday", "sunday"}
+ >>> p.skip_days = {"Friday", "Saturday", "sUnDaY"}
>>> p.skip_days
{"Saturday", "Friday", "Sunday"}
+ :type: :obj:`set` of :obj:`str`
+ :RSS: skipDays
"""
return self.__skip_days
@@ -780,6 +860,9 @@ def skip_days(self, days):
def web_master(self):
"""The :class:`~podgen.Person` responsible for
technical issues relating to the feed.
+
+ :type: :class:`podgen.Person`
+ :RSS: webMaster
"""
return self.__web_master
@@ -794,9 +877,10 @@ def web_master(self, web_master):
@property
def category(self):
"""The iTunes category, which appears in the category column
- and in iTunes Store Browser.
+ and in iTunes Store listings.
- Use the :class:`podgen.Category` class.
+ :type: :class:`podgen.Category`
+ :RSS: itunes:category
"""
return self.__category
@@ -815,22 +899,24 @@ def category(self, category):
@property
def image(self):
- """The image for the podcast. This tag specifies the artwork
- for your podcast. Put the URL to the image in the href attribute. iTunes
- prefers square .jpg images that are at least 1400x1400 pixels, which is
- different from what is specified for the standard RSS image tag. In order
- for a podcast to be eligible for an iTunes Store feature, the
- accompanying image must be at least 1400x1400 pixels.
+ """The URL of the artwork for this podcast. iTunes
+ prefers square images that are at least ``1400x1400`` pixels.
+ Podcasts with an image smaller than this are *not* eligible to be
+ featured on the iTunes Store.
iTunes supports images in JPEG and PNG formats with an RGB color space
- (CMYK is not supported). The URL must end in ".jpg" or ".png". If the
- tag is not present, iTunes will use the contents of the
- RSS image tag.
-
- If you change your podcast’s image, also change the file’s name. iTunes
- may not change the image if it checks your feed and the image URL is the
- same. The server hosting your cover art image must allow HTTP head
- requests for iTunes to be able to automatically update your cover art.
+ (CMYK is not supported). The URL must end in ".jpg" or ".png".
+
+ :type: :obj:`str`
+ :RSS: itunes:image
+
+ .. note::
+
+ If you change your podcast’s image, you must also change the file’s
+ name; iTunes doesn't check the image to see if it has changed.
+
+ Additionally, the server hosting your cover art image must allow HTTP
+ HEAD requests (most servers support this).
"""
return self.__image
@@ -853,11 +939,14 @@ def complete(self):
episodes will be added to the podcast. If you let this be ``None`` or
``False``, you are indicating that new episodes may be posted.
+ :type: :obj:`bool`
+ :RSS: itunes:complete
+
.. warning::
Setting this to ``True`` is the same as promising you'll never ever
release a new episode. Do NOT set this to ``True`` as long as
- there's any chance at all that a new episode will be released
+ there's any chance AT ALL that a new episode will be released
someday.
"""
@@ -873,11 +962,14 @@ def complete(self, complete):
@property
def owner(self):
"""The :class:`~podgen.Person` who owns this podcast. iTunes
- will use this information to contact the owner of the podcast for
- communication specifically about the podcast. It will not be publicly
- displayed, but it will be in the feed source.
+ will use this person's name and email address for all correspondence
+ related to this podcast. It will not be publicly displayed, but it's
+ still publicly available in the RSS source.
Both the name and email are required.
+
+ :type: :class:`podgen.Person`
+ :RSS: itunes:owner
"""
return self.__owner
@@ -898,6 +990,9 @@ def feed_url(self):
Identifying a feed's URL within the feed makes it more portable,
self-contained, and easier to cache. You should therefore set this
attribute if you're able to.
+
+ :type: :obj:`str`
+ :RSS: atom:link with ``rel="self"``
"""
return self.__feed_url
diff --git a/podgen/tests/test_podcast.py b/podgen/tests/test_podcast.py
index 65b49f6..a0faf5c 100644
--- a/podgen/tests/test_podcast.py
+++ b/podgen/tests/test_podcast.py
@@ -191,7 +191,7 @@ def test_generator(self):
assert self.programname in generator
# Using set_generator, text excludes python-podgen
- self.fg.set_generator(software_name, exclude_feedgen=True)
+ 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
@@ -417,5 +417,17 @@ def test_withholdFromItunes(self):
.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
+
if __name__ == '__main__':
unittest.main()
diff --git a/podgen/util.py b/podgen/util.py
index 7c3aab3..196eb39 100644
--- a/podgen/util.py
+++ b/podgen/util.py
@@ -23,6 +23,7 @@ def ensure_format(val, allowed, required, allowed_values=None, defaults=None):
:param defaults: Dictionary with default values.
:returns: List of checked dictionaries.
"""
+ # TODO: Check if this function is obsolete and perhaps remove it
if not val:
return None
if allowed_values is None:
@@ -63,7 +64,16 @@ def ensure_format(val, allowed, required, allowed_values=None, defaults=None):
def formatRFC2822(d):
- """Make sure the locale setting do not interfere with the time format.
+ """Format a datetime according to RFC2822.
+
+ This implementation exists as a workaround to ensure that the locale setting
+ does not interfere with the time format. For example, day names might get
+ translated to your local language, which would break with the standard.
+
+ :param d: Time and date you want to format according to RFC2822.
+ :type d: datetime.datetime
+ :returns: The datetime formatted according to the RFC2822.
+ :rtype: str
"""
l = locale.setlocale(locale.LC_ALL)
locale.setlocale(locale.LC_ALL, 'C')
@@ -79,14 +89,34 @@ def formatRFC2822(d):
import cgi
def htmlencode(s):
+ """Encode the given string so its content won't be confused as HTML
+ markup.
+
+ This function exists as a cross-version compatibility alias."""
return cgi.escape(s, quote=True)
else:
import html
def htmlencode(s):
+ """Encode the given string so its content won't be confused as HTML
+ markup.
+
+ This function exists as a cross-version compatibility alias."""
return html.escape(s)
+
def listToHumanreadableStr(l):
+ """Create a human-readable string out of the given iterable.
+
+ Example::
+
+ >>> from podgen.util import listToHumanreadableStr
+ >>> listToHumanreadableStr([1, 2, 3])
+ 1, 2 and 3
+
+ The string ``(empty)`` is returned if the list is empty – it is assumed
+ that you check whether the list is empty yourself.
+ """
# TODO: Allow translations of "and" and "empty"
length = len(l)
l = [str(e) for e in l]
diff --git a/podgen/version.py b/podgen/version.py
index b492c7d..ccacf85 100644
--- a/podgen/version.py
+++ b/podgen/version.py
@@ -25,7 +25,7 @@
version_full_str = '.'.join([str(x) for x in version_full])
'Name of this project'
-name = "python-podgen (podcastgen)"
+name = "python-podgen"
'Website of this project'
-website = "https://github.com/tobinus/python-podgen/tree/podcastgen"
+website = "https://podgen.readthedocs.org"
diff --git a/readme.md b/readme.md
index d9dfdfa..d3d14ad 100644
--- a/readme.md
+++ b/readme.md
@@ -2,7 +2,10 @@
PodGen (forked from python-feedgen)
===================================
-[![Build Status](https://travis-ci.org/tobinus/python-podgen.svg?branch=master)](https://travis-ci.org/tobinus/python-podgen) [![Documentation Status](https://readthedocs.org/projects/podgen/badge/?version=latest)](http://podgen.readthedocs.io/en/latest/?badge=latest)
+[![Build Status](https://travis-ci.org/tobinus/python-podgen.svg?branch=master)](https://travis-ci.org/tobinus/python-podgen)
+[![Documentation Status](https://readthedocs.org/projects/podgen/badge/?version=latest)](http://podgen.readthedocs.io/en/latest/?badge=latest)
+[![Stories in Ready](https://badge.waffle.io/tobinus/python-podgen.svg?label=ready&title=Ready)](http://waffle.io/tobinus/python-podgen)
+
This module can be used to generate podcast feeds in RSS format, and is
compatible with Python 3.3+.
@@ -14,17 +17,12 @@ at license.bsd and license.lgpl.
More details about the project:
- Repository: https://github.com/tobinus/python-podgen
-- Documentation: http://lkiesow.github.io/python-feedgen/
+- Documentation: https://podgen.readthedocs.io/
- Python Package Index: https://pypi.python.org/pypi/podgen/
-------------
-Installation
-------------
-
-Currently, you'll need to clone this repository, and create a virtualenv and
-install lxml and dateutils.
-
+See the documentation link above for installation instructions and
+guides on how to use this module.
----------
Known bugs
diff --git a/requirements.txt b/requirements.txt
index e92b9b9..db067d1 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,4 @@
+# Remember to add any new requirements to setup.py as well!
dateutils
lxml
pytz
diff --git a/setup.py b/setup.py
index bba2731..e6e9815 100755
--- a/setup.py
+++ b/setup.py
@@ -9,15 +9,14 @@
packages = ['podgen'],
version = podgen.version.version_full_str,
description = 'Generating podcasts with Python should be easy!',
- author = 'Lars Kiesow',
- author_email = 'lkiesow@uos.de',
- url = 'http://lkiesow.github.io/python-feedgen',
+ author = 'Thorben W. S. Dahl',
+ author_email = 'thorben@sjostrom.no',
+ url = 'http://podgen.readthedocs.io/en/latest/',
keywords = ['feed','RSS','podcast','iTunes'],
license = 'FreeBSD and LGPLv3+',
- install_requires = ['lxml', 'dateutils'],
+ install_requires = ['lxml', 'dateutils', 'future', 'pytz'],
classifiers = [
'Development Status :: 4 - Beta',
- 'Development Status :: 5 - Production/Stable',
'Intended Audience :: Developers',
'Intended Audience :: Information Technology',
'Intended Audience :: Science/Research',
@@ -26,9 +25,13 @@
'Natural Language :: English',
'Operating System :: OS Independent',
'Programming Language :: Python',
- 'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 3 :: Only',
+ 'Programming Language :: Python :: 3.3',
+ 'Programming Language :: Python :: 3.4',
+ 'Programming Language :: Python :: 3.5',
'Topic :: Communications',
'Topic :: Internet',
+ 'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: Text Processing',
'Topic :: Text Processing :: Markup',
'Topic :: Text Processing :: Markup :: XML'
@@ -40,9 +43,10 @@
This module can be used to easily generate Podcasts. It is designed so you
don't need to read up on how RSS and iTunes functions – it just works!
-See the documentation at ....
+See the documentation at http://podgen.readthedocs.io/en/latest/ for more
+information.
-It is licensed under the terms of both, the FreeBSD license and the LGPLv3+.
+It is licensed under the terms of both the FreeBSD license and the LGPLv3+.
Choose the one which is more convenient for you. For more details have a look
at license.bsd and license.lgpl.
'''