Feeds.Py
The markata.plugins.feeds
plugin is used to create feed pages, which are lists of
posts. The list is generated using a filter
, then each post in the list is
rendered with a card_template
before being applied to the body
of the
template
.
Installation
This plugin is built-in and enabled by default, but in you want to be very explicit you can add it to your list of existing plugins.
hooks = [ "markata.plugins.feeds", ]
Configuration
set default template and card_template
At the root of the markata.feeds config you may set template
, and
card_template
. These will become your defaults for every feed you create.
If you do not set these, markata will use it's defaults. The defaults are
designed to work for a variety of use cases, but are not likely the best for
all.
[markata.feeds_config] template="pages/templates/archive_template.html" card_template="plugins/feed_card_template.html"
pages
Underneath of the markata.feeds
we will create a new map for each page where
the name of the map will be the name of the page.
The following config will create a page at /all-posts
that inclues every
single post.
[[markata.feeds]] title="All Posts" slug='all' filter="True"
template
The template
configuration key is a file path to the template that you want
to use to create the feed. You may set the default template you want to use
for all feeds under [markata.feeds]
, as well as override it inside of each
feeds config.
The template is a jinja style template that expects to fill in a title
and
body
variable.
<!DOCTYPE html> <html lang="en"> <head> <title>{{ title }}</title> </head> <body> <ul> {{ body }} </ul> </body> </html>
Note
I highly reccomend putting your body
in a <ul>
, and wrapping your
card_template
s in an <li>
.
card_template
All keys available from each post is available to put into your jinja template. These can either be placed there in your post frontmatter, or through a plugin that automatically adds to the post before the save phase.
Here is a very simple example that would give a link to each post with the title and date.
[[markata.feeds]] slug='all' title='All Posts' filter="True" card_template=''' <li> <a href={{markata.config.get('path_prefix', '')}}{{slug}}> {{title}}-{{date}} </a> </li> '''
filter
The filter is a python expression ran on every post that expects to return a
boolean. The variables available to this expression are every key in your
frontmatter, plus the timedelta
function, and parse
function to more easily
work with dates.
Feed Examples
True can be passed in to make a feed of all the posts you have.
[[markata.feeds]] slug='all' title='All Posts' filter="True"
You can compare against the values of the keys from your frontmatter. This
example creates a feed that includes every post where published is True
.
[[markata.feeds]] slug='draft' title='Draft' filter="published=='False'"
We can also compare against dates. The
markata.plugins.datetime
plugin, automatically adds today
as today's date and now
as the current
datetime. These are quite handy to create feeds for scheduled, recent, or
today's posts. The following two examples will create a feed for scheduled
posts and for today's posts respectively.
[[markata.feeds]] slug='scheduled' title='Scheduled' filter="date>today" [[markata.feeds]] slug='today' title='Today' filter="date==today"
If you have list of items in your frontmatter for something like tags
, you
can check for the existence of a tag in the list.
[[markata.feeds]] slug='python' title='Python' filter="date<=today and 'python' in tags"
And of course you can combine all the things into larger expressions. Here is one example of the main feed on my blog.
[[markata.feeds]] slug='blog' title='Blog' filter="date<=today and templateKey in ['blog-post'] and published =='True'"
Here is another example that shows my drafts for a particular tag.
[[markata.feeds]] slug='python-draft' title='Python Draft' filter="date<=today and 'python' in tags and published=='False'"
Defaults
By default feeds will create one feed page at /archive/
that includes all
posts.
markata.feeds slug='archive' title='All Posts' filter="True"
!! class
SilentUndefined class
SilentUndefined source
class SilentUndefined(Undefined): def _fail_with_undefined_error(self, *args, **kwargs): return ""
!! class
MarkataFilterError class
MarkataFilterError source
class MarkataFilterError(RuntimeError): ...
!! class
FeedConfig class
FeedConfig source
class FeedConfig(pydantic.BaseModel, JupyterMixin): DEFAULT_TITLE: str = "All Posts" title: str = DEFAULT_TITLE slug: str = None description: Optional[str] = None name: Optional[str] = None filter: str = "True" sort: str = "date" reverse: bool = False head: Optional[int] = None tail: Optional[int] = None rss: bool = True sitemap: bool = True card_template: str = "card.html" template: str = "feed.html" partial_template: str = "feed_partial.html" rss_template: str = "rss.xml" sitemap_template: str = "sitemap.xml" xsl_template: str = "rss.xsl" @pydantic.validator("name", pre=True, always=True) def default_name(cls, v, *, values): return v or str(values.get("slug")).replace("-", "_") @property def __rich_console__(self) -> "Console": return self.markata.console @property def __rich__(self) -> Pretty: return lambda: Pretty(self)
!! class
FeedsConfig class
FeedsConfig source
class FeedsConfig(pydantic.BaseModel): feeds: List[FeedConfig] = [FeedConfig(slug="archive")]
!! class
PrettyList class
PrettyList source
class PrettyList(list, JupyterMixin): def _repr_pretty_(self): return self.__rich__() def __rich__(self) -> Pretty: return Pretty(self)
!! class
Feed class
A storage class for markata feed objects.# Usage
``` python
from markata import Markata
m = Markata()
# access posts for a feed
m.feeds.docs.posts
# access config for a feed
m.feeds.docs.config
```
Feed source
class Feed(JupyterMixin): """ A storage class for markata feed objects. # Usage ``` python from markata import Markata m = Markata() # access posts for a feed m.feeds.docs.posts # access config for a feed m.feeds.docs.config ``` """ config: FeedConfig _m: Markata @property def __rich_console__(self) -> "Console": return self._m.console @property def name(self): return self.config.name @property def posts(self): posts = self.map("post") if self.config.head is not None and self.config.tail is not None: head_posts = posts[: self.config.head] tail_posts = posts[-self.config.tail :] return PrettyList(head_posts + tail_posts) if self.config.head is not None: return PrettyList(posts[: self.config.head]) if self.config.tail is not None: return PrettyList(posts[-self.config.tail :]) return PrettyList(posts) def first( self: "Markata", ) -> list: return self.posts[0] def last( self: "Markata", ) -> list: return self.posts[-1] def map(self, func="post", **args): return self._m.map(func, **{**self.config.dict(), **args}) def __rich__(self) -> Table: table = Table(title=f"Feed: {self.name}") table.add_column("Post", justify="right", style="cyan", no_wrap=True) table.add_column("slug", justify="left", style="green") table.add_column("published", justify="left", style="green") for post in self.posts: table.add_row(post.title, post.slug, str(post.published)) return table
!! class
Feeds class
A storage class for all markata Feed objects``` python
from markata import Markata
m = Markata()
m.feeds
# access all config
m.feeds.config
# refresh list of posts in all feeds
m.feeds.refresh()
# iterating over feeds gives the name of the feed
for k in m.feeds:
print(k)
# project-gallery
# docs
# autodoc
# core_modules
# plugins
# archive
# iterate over items like keys and values in a dict, items returns name of
# feed and a feed object
for k, v in m.feeds.items():
print(k, len(v.posts))
# project-gallery 2
# docs 6
# autodoc 65
# core_modules 26
# plugins 39
# archive 65
# values can be iterated over in just the same way
for v in m.feeds.values():
print(len(v.posts))
# 2
# 6
# 65
# 26
# 39
# 65
```
Accessing feeds can be done using square brackets or dot notation.
``` python
from markata import Markata
m = Markata()
# both of these will return the `docs` Feed object.
m.feeds.docs
m['docs']
```
Feeds source
class Feeds(JupyterMixin): """ A storage class for all markata Feed objects ``` python from markata import Markata m = Markata() m.feeds # access all config m.feeds.config # refresh list of posts in all feeds m.feeds.refresh() # iterating over feeds gives the name of the feed for k in m.feeds: print(k) # project-gallery # docs # autodoc # core_modules # plugins # archive # iterate over items like keys and values in a dict, items returns name of # feed and a feed object for k, v in m.feeds.items(): print(k, len(v.posts)) # project-gallery 2 # docs 6 # autodoc 65 # core_modules 26 # plugins 39 # archive 65 # values can be iterated over in just the same way for v in m.feeds.values(): print(len(v.posts)) # 2 # 6 # 65 # 26 # 39 # 65 ``` Accessing feeds can be done using square brackets or dot notation. ``` python from markata import Markata m = Markata() # both of these will return the `docs` Feed object. m.feeds.docs m['docs'] ``` """ def __init__(self, markata: Markata) -> None: self._m = markata self.config = {f.name: f for f in markata.config.feeds} self.refresh() def refresh(self) -> None: """ Refresh all of the feeds objects """ for feed in self._m.config.feeds: feed = Feed(config=feed, _m=self._m) self.__setattr__(feed.name, feed) def __iter__(self): return iter(self.config.keys()) def keys(self): return iter(self.config.keys()) def values(self): return [self[feed] for feed in self.config.keys()] def items(self): return [(key, self[key]) for key in self.config] def __getitem__(self, key: str) -> Any: return getattr(self, key.replace("-", "_").lower()) def get(self, key: str, default: Any = None) -> Any: return getattr(self, key.replace("-", "_").lower(), default) def _dict_panel(self, config) -> str: """ pretty print configs with rich """ msg = "" for key, value in config.items(): if isinstance(value, str): if len(value) > 50: value = value[:50] + "..." value = value msg = msg + f"[grey46]{key}[/][magenta3]:[/] [grey66]{value}[/]\n" return msg def __rich__(self) -> Table: table = Table(title=f"Feeds {len(self.config)}") table.add_column("Feed", justify="right", style="cyan", no_wrap=True) table.add_column("posts", justify="left", style="green") table.add_column("config", style="magenta") for name in self.config: table.add_row( name, str(len(self[name].posts)), self._dict_panel(self.config[name].dict()), ) return table
!! function
config_model function
config_model source
def config_model(markata: Markata) -> None: markata.config_models.append(FeedsConfig)
!! function
pre_render function
Create the Feeds object and attach it to markata.pre_render source
def pre_render(markata: Markata) -> None: """ Create the Feeds object and attach it to markata. """ markata.feeds = Feeds(markata)
!! function
get_template function
get_template source
def get_template(markata, template): try: return markata.config.jinja_env.get_template(template) except jinja2.TemplateNotFound: # try to load it as a file ... try: return Template(Path(template).read_text(), undefined=SilentUndefined) except FileNotFoundError: # default to load it as a string ... except OSError: # thrown by File name too long # default to load it as a string ... return Template(template, undefined=SilentUndefined)
!! function
save function
Creates a new feed page for each page in the config.save source
def save(markata: Markata) -> None: """ Creates a new feed page for each page in the config. """ with markata.cache as cache: for feed in markata.feeds.values(): create_page( markata, feed, cache, ) home = Path(str(markata.config.output_dir)) / "index.html" archive = Path(str(markata.config.output_dir)) / "archive" / "index.html" if not home.exists() and archive.exists(): shutil.copy(str(archive), str(home)) xsl_template = get_template(markata, feed.config.xsl_template) xsl = xsl_template.render( markata=markata, __version__=__version__, today=datetime.datetime.today(), config=markata.config, ) xsl_file = Path(markata.config.output_dir) / "rss.xsl" xsl_file.write_text(xsl)
!! function
create_page function
create an html unorderd list of posts.create_page source
def create_page( markata: Markata, feed: Feed, cache, ) -> None: """ create an html unorderd list of posts. """ template = get_template(markata, feed.config.template) partial_template = get_template(markata, feed.config.partial_template) canonical_url = f"{markata.config.url}/{feed.config.slug}/" key = markata.make_hash( "feeds", template, __version__, # cards, markata.config.url, markata.config.description, feed.config.title, canonical_url, # datetime.datetime.today(), # markata.config, ) html_key = markata.make_hash(key, "html") html_partial_key = markata.make_hash(key, "partial_html") feed_rss_key = markata.make_hash(key, "rss") feed_sitemap_key = markata.make_hash(key, "sitemap") feed_html_from_cache = markata.precache.get(html_key) feed_html_partial_from_cache = markata.precache.get(html_partial_key) feed_rss_from_cache = markata.precache.get(feed_rss_key) feed_sitemap_from_cache = markata.precache.get(feed_sitemap_key) output_file = Path(markata.config.output_dir) / feed.config.slug / "index.html" output_file.parent.mkdir(exist_ok=True, parents=True) partial_output_file = ( Path(markata.config.output_dir) / feed.config.slug / "partial" / "index.html" ) partial_output_file.parent.mkdir(exist_ok=True, parents=True) rss_output_file = Path(markata.config.output_dir) / feed.config.slug / "rss.xml" rss_output_file.parent.mkdir(exist_ok=True, parents=True) sitemap_output_file = ( Path(markata.config.output_dir) / feed.config.slug / "sitemap.xml" ) sitemap_output_file.parent.mkdir(exist_ok=True, parents=True) if feed_html_from_cache is None: feed_html = template.render( markata=markata, __version__=__version__, post=feed.config.model_dump(), url=markata.config.url, config=markata.config, feed=feed, ) cache.set(html_key, feed_html) else: feed_html = feed_html_from_cache if feed_html_partial_from_cache is None: feed_html_partial = partial_template.render( markata=markata, __version__=__version__, post=feed.config.model_dump(), url=markata.config.url, config=markata.config, feed=feed, ) cache.set(html_partial_key, feed_html_partial) else: feed_html_partial = feed_html_partial_from_cache if feed_rss_from_cache is None: rss_template = get_template(markata, feed.config.rss_template) feed_rss = rss_template.render(markata=markata, feed=feed) cache.set(feed_rss_key, feed_rss) else: feed_rss = feed_rss_from_cache if feed_sitemap_from_cache is None: sitemap_template = get_template(markata, feed.config.sitemap_template) feed_sitemap = sitemap_template.render(markata=markata, feed=feed) cache.set(feed_sitemap_key, feed_sitemap) else: feed_sitemap = feed_sitemap_from_cache output_file.write_text(feed_html) partial_output_file.write_text(feed_html_partial) rss_output_file.write_text(feed_rss) sitemap_output_file.write_text(feed_sitemap)
!! function
create_card function
Creates a card for one post based on the configured template. If no template is configured it will create one with the post title and dates (if present).create_card source
def create_card( markata: "Markata", post: "Post", template: Optional[str] = None, cache=None, ) -> Any: """ Creates a card for one post based on the configured template. If no template is configured it will create one with the post title and dates (if present). """ key = markata.make_hash("feeds", template, str(post), post.content) card = markata.precache.get(key) if card is not None: return card if template is None: template = markata.config.get("feeds_config", {}).get("card_template", None) if template is None: if "date" in post: card = textwrap.dedent( f""" <li class='post'> <a href="/{markata.config.path_prefix}{post.slug}/"> {post.title} {post.date.year}- {post.date.month}- {post.date.day} </a> </li> """, ) else: card = textwrap.dedent( f""" <li class='post'> <a href="/{markata.config.path_prefix}{post.slug}/"> {post.title} </a> </li> """, ) else: try: _template = Template(Path(template).read_text()) except FileNotFoundError: _template = Template(template) except OSError: # File name too long _template = Template(template) card = _template.render(post=post, **post.to_dict()) cache.add(key, card) return card
!! function
cli function
cli source
def cli(app: typer.Typer, markata: "Markata") -> None: feeds_app = typer.Typer() app.add_typer(feeds_app) @feeds_app.callback() def feeds(): "feeds cli" @feeds_app.command() def show() -> None: markata.console.quiet = True feeds = markata.feeds markata.console.quiet = False markata.console.print("Feeds") markata.console.print(feeds)
!! method
_fail_with_undefined_error method
_fail_with_undefined_error source
def _fail_with_undefined_error(self, *args, **kwargs): return ""
!! method
default_name method
default_name source
def default_name(cls, v, *, values): return v or str(values.get("slug")).replace("-", "_")
!! method
rich_console method
rich_console source
def __rich_console__(self) -> "Console": return self.markata.console
!! method
rich method
rich source
def __rich__(self) -> Pretty: return lambda: Pretty(self)
!! method
repr_pretty method
repr_pretty source
def _repr_pretty_(self): return self.__rich__()
!! method
rich method
rich source
def __rich__(self) -> Pretty: return Pretty(self)
!! method
rich_console method
rich_console source
def __rich_console__(self) -> "Console": return self._m.console
!! method
name method
name source
def name(self): return self.config.name
!! method
posts method
posts source
def posts(self): posts = self.map("post") if self.config.head is not None and self.config.tail is not None: head_posts = posts[: self.config.head] tail_posts = posts[-self.config.tail :] return PrettyList(head_posts + tail_posts) if self.config.head is not None: return PrettyList(posts[: self.config.head]) if self.config.tail is not None: return PrettyList(posts[-self.config.tail :]) return PrettyList(posts)
!! method
first method
first source
def first( self: "Markata", ) -> list: return self.posts[0]
!! method
last method
last source
def last( self: "Markata", ) -> list: return self.posts[-1]
!! method
map method
map source
def map(self, func="post", **args): return self._m.map(func, **{**self.config.dict(), **args})
!! method
rich method
rich source
def __rich__(self) -> Table: table = Table(title=f"Feed: {self.name}") table.add_column("Post", justify="right", style="cyan", no_wrap=True) table.add_column("slug", justify="left", style="green") table.add_column("published", justify="left", style="green") for post in self.posts: table.add_row(post.title, post.slug, str(post.published)) return table
!! method
init method
init source
def __init__(self, markata: Markata) -> None: self._m = markata self.config = {f.name: f for f in markata.config.feeds} self.refresh()
!! method
refresh method
Refresh all of the feeds objectsrefresh source
def refresh(self) -> None: """ Refresh all of the feeds objects """ for feed in self._m.config.feeds: feed = Feed(config=feed, _m=self._m) self.__setattr__(feed.name, feed)
!! method
iter method
iter source
def __iter__(self): return iter(self.config.keys())
!! method
keys method
keys source
def keys(self): return iter(self.config.keys())
!! method
values method
values source
def values(self): return [self[feed] for feed in self.config.keys()]
!! method
items method
items source
def items(self): return [(key, self[key]) for key in self.config]
!! method
getitem method
getitem source
def __getitem__(self, key: str) -> Any: return getattr(self, key.replace("-", "_").lower())
!! method
get method
get source
def get(self, key: str, default: Any = None) -> Any: return getattr(self, key.replace("-", "_").lower(), default)
!! method
_dict_panel method
pretty print configs with rich_dict_panel source
def _dict_panel(self, config) -> str: """ pretty print configs with rich """ msg = "" for key, value in config.items(): if isinstance(value, str): if len(value) > 50: value = value[:50] + "..." value = value msg = msg + f"[grey46]{key}[/][magenta3]:[/] [grey66]{value}[/]\n" return msg
!! method
rich method
rich source
def __rich__(self) -> Table: table = Table(title=f"Feeds {len(self.config)}") table.add_column("Feed", justify="right", style="cyan", no_wrap=True) table.add_column("posts", justify="left", style="green") table.add_column("config", style="magenta") for name in self.config: table.add_row( name, str(len(self[name].posts)), self._dict_panel(self.config[name].dict()), ) return table
!! function
feeds function
feeds clifeeds source
def feeds(): "feeds cli"
!! function
show function
show source
def show() -> None: markata.console.quiet = True feeds = markata.feeds markata.console.quiet = False markata.console.print("Feeds") markata.console.print(feeds)