Skip to content

mkdocs_newsletter

Automatically create newsletters from the changes in a mkdocs repository.

Change

Bases: BaseModel

Represent a single semantic change in a git repository.

Attributes:

Name Type Description
date datetime

When the change was done.

summary str

short description of the change.

type_ Optional[str]

semantic type of change, such as feature or fix.

message Optional[str]

long description of the change.

breaking bool

if the change breaks previous functionality.

category Optional[str]

name of the group of files that share meaning.

category_order Optional[int]

order of the category against all categories.

subcategory Optional[str]

name of the subgroup of files that share meaning.

category_order Optional[int]

order of the subcategory against all subcategories.

file_ Optional[str]

markdown file name.

file_section Optional[str]

title of the file containing the change.

file_section_order Optional[int]

order of the file in the subcategory or category that holds the file.

file_subsection Optional[str]

title of the section of the file the change belongs to.

Source code in mkdocs_newsletter/model.py
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class Change(BaseModel):
    """Represent a single semantic change in a git repository.

    Attributes:
        date: When the change was done.
        summary: short description of the change.
        type_: semantic type of change, such as feature or fix.
        message: long description of the change.
        breaking: if the change breaks previous functionality.
        category: name of the group of files that share meaning.
        category_order: order of the category against all categories.
        subcategory: name of the subgroup of files that share meaning.
        category_order: order of the subcategory against all subcategories.
        file_: markdown file name.
        file_section: title of the file containing the change.
        file_section_order: order of the file in the subcategory or category that holds
            the file.
        file_subsection: title of the section of the file the change belongs to.
    """

    date: datetime
    summary: str
    scope: Optional[str]
    type_: Optional[str]
    message: Optional[str] = None
    breaking: bool = False
    publish: Optional[bool] = None
    category: Optional[str] = None
    category_order: Optional[int] = None
    subcategory: Optional[str] = None
    subcategory_order: Optional[int] = None
    file_: Optional[str] = None
    file_section: Optional[str] = None
    file_section_order: Optional[int] = None
    file_subsection: Optional[str] = None

Newsletter

Bases: BasePlugin

Define the MkDocs plugin to create newsletters.

Source code in mkdocs_newsletter/entrypoints/mkdocs_plugin.py
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
class Newsletter(BasePlugin):  # type: ignore
    """Define the MkDocs plugin to create newsletters."""

    def __init__(self) -> None:
        """Initialize the basic attributes.

        Attributes:
            repo: Git repository to analyze.
        """
        self.working_dir = os.getenv("NEWSLETTER_WORKING_DIR", default=os.getcwd())
        self.repo = Repo(self.working_dir)

    def on_config(self, config: Optional[MkDocsConfig]) -> MkDocsConfig:
        """Create the new newsletters and load them in the navigation.

        Through the following steps:

        * Detect which were the last changes for each of the feeds.
        * Parse the changes from the git history that were done before the last
            changes.
        * Create the newsletter articles.
        * Update the navigation.

        Args:
            config: MkDocs global configuration object.

        Returns:
            config: MkDocs config object with the new newsletters in the Newsletter
                section.
        """
        if config is None:
            config = MkDocsConfig()
        newsletter_dir = f"{self.working_dir}/docs/newsletter"
        if not os.path.exists(newsletter_dir):
            os.makedirs(newsletter_dir)
        last_published_changes = last_newsletter_changes(newsletter_dir)
        changes_to_publish = add_change_categories(
            semantic_changes(self.repo, last_published_changes.min()), config
        )
        changes_per_feed = digital_garden_changes(
            changes_to_publish,
            last_published_changes,
        )

        create_newsletters(changes_per_feed, self.repo)
        create_newsletter_landing_page(config, self.repo)

        config = build_nav(config, newsletter_dir)

        return config

    # The * in the signature is to mimic the parent class signature
    def on_post_build(self, *, config: MkDocsConfig) -> None:
        """Create the RSS feeds."""
        create_rss(config, self.working_dir)

__init__()

Initialize the basic attributes.

Attributes:

Name Type Description
repo

Git repository to analyze.

Source code in mkdocs_newsletter/entrypoints/mkdocs_plugin.py
27
28
29
30
31
32
33
34
def __init__(self) -> None:
    """Initialize the basic attributes.

    Attributes:
        repo: Git repository to analyze.
    """
    self.working_dir = os.getenv("NEWSLETTER_WORKING_DIR", default=os.getcwd())
    self.repo = Repo(self.working_dir)

on_config(config)

Create the new newsletters and load them in the navigation.

Through the following steps:

  • Detect which were the last changes for each of the feeds.
  • Parse the changes from the git history that were done before the last changes.
  • Create the newsletter articles.
  • Update the navigation.

Parameters:

Name Type Description Default
config Optional[MkDocsConfig]

MkDocs global configuration object.

required

Returns:

Name Type Description
config MkDocsConfig

MkDocs config object with the new newsletters in the Newsletter section.

Source code in mkdocs_newsletter/entrypoints/mkdocs_plugin.py
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
def on_config(self, config: Optional[MkDocsConfig]) -> MkDocsConfig:
    """Create the new newsletters and load them in the navigation.

    Through the following steps:

    * Detect which were the last changes for each of the feeds.
    * Parse the changes from the git history that were done before the last
        changes.
    * Create the newsletter articles.
    * Update the navigation.

    Args:
        config: MkDocs global configuration object.

    Returns:
        config: MkDocs config object with the new newsletters in the Newsletter
            section.
    """
    if config is None:
        config = MkDocsConfig()
    newsletter_dir = f"{self.working_dir}/docs/newsletter"
    if not os.path.exists(newsletter_dir):
        os.makedirs(newsletter_dir)
    last_published_changes = last_newsletter_changes(newsletter_dir)
    changes_to_publish = add_change_categories(
        semantic_changes(self.repo, last_published_changes.min()), config
    )
    changes_per_feed = digital_garden_changes(
        changes_to_publish,
        last_published_changes,
    )

    create_newsletters(changes_per_feed, self.repo)
    create_newsletter_landing_page(config, self.repo)

    config = build_nav(config, newsletter_dir)

    return config

on_post_build(*, config)

Create the RSS feeds.

Source code in mkdocs_newsletter/entrypoints/mkdocs_plugin.py
76
77
78
def on_post_build(self, *, config: MkDocsConfig) -> None:
    """Create the RSS feeds."""
    create_rss(config, self.working_dir)

digital_garden_changes(changes, last_published=None)

Extract the changes that need to be published for digital_garden repositories.

For a change to be published it needs to:

Be made before the first day of the year and after the last published change

in the year feed.

Be made before the first day of the month and after the last published change

in the month feed.

Be made before the last Monday and after the last published change in the

week feed.

Parameters:

Name Type Description Default
changes List[Change]

The list of Change objects to publish.

required
last_published Optional[LastNewsletter]

last published date per feed type

None

Returns:

Name Type Description
changes DigitalGardenChanges

Ordered changes to publish per feed.

Source code in mkdocs_newsletter/services/newsletter.py
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
def digital_garden_changes(
    changes: List[Change], last_published: Optional[LastNewsletter] = None
) -> DigitalGardenChanges:
    """Extract the changes that need to be published for digital_garden repositories.

    For a change to be published it needs to:

    year: Be made before the first day of the year and after the last published change
        in the year feed.
    month: Be made before the first day of the month and after the last published change
        in the month feed.
    week: Be made before the last Monday and after the last published change in the
        week feed.
    day: Be made before today and after the last published change in the day feed.

    Args:
        changes: The list of Change objects to publish.
        last_published: last published date per feed type

    Returns:
        changes: Ordered changes to publish per feed.
    """
    now = datetime.datetime.now(tz.tzlocal())
    today = now.replace(hour=0, minute=0, second=0, microsecond=0)
    last_first_weekday = today - datetime.timedelta(days=now.weekday())
    last_first_monthday = today.replace(day=1)
    last_first_yearday = today.replace(day=1, month=1)

    if last_published is None:
        last_published = LastNewsletter()

    return DigitalGardenChanges(
        daily=[
            change
            for change in changes
            if change.date < today
            and (last_published.daily is None or change.date > last_published.daily)
            and change.type_ in CHANGE_TYPE_TEXT
        ],
        weekly=[
            change
            for change in changes
            if change.date < last_first_weekday
            and (last_published.weekly is None or change.date > last_published.weekly)
            and change.type_ in CHANGE_TYPE_TEXT
        ],
        monthly=[
            change
            for change in changes
            if change.date < last_first_monthday
            and (last_published.monthly is None or change.date > last_published.monthly)
            and change.type_ in CHANGE_TYPE_TEXT
        ],
        yearly=[
            change
            for change in changes
            if change.date < last_first_yearday
            and (last_published.yearly is None or change.date > last_published.yearly)
            and change.type_ in CHANGE_TYPE_TEXT
        ],
    )

last_newsletter_changes(newsletter_dir)

Extract the date of the last change of the last newsletter for each feed.

Parameters:

Name Type Description Default
newsletter_dir str

Directory containing the newsletter articles.

required

Returns:

Name Type Description
last_newsletter LastNewsletter

LastNewsletter object.

Source code in mkdocs_newsletter/services/newsletter.py
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
def last_newsletter_changes(newsletter_dir: str) -> LastNewsletter:
    """Extract the date of the last change of the last newsletter for each feed.

    Args:
        newsletter_dir: Directory containing the newsletter articles.

    Returns:
        last_newsletter: LastNewsletter object.
    """
    newsletters = _list_newsletters(newsletter_dir)
    last = LastNewsletter()

    # Year feed: Saves the first day of the next year.
    with suppress(IndexError):
        last.yearly = newsletters.yearly[0].date + relativedelta(years=1)

    # Month feed: Saves the first day of the next month.
    with suppress(IndexError):
        last_file_date = newsletters.monthly[0].date
        last.monthly = datetime.datetime(
            last_file_date.year + int(last_file_date.month / 12),
            ((last_file_date.month % 12) + 1),
            1,
            tzinfo=tz.tzlocal(),
        )

    # Week feed: Saves the next Monday from the week of the week number.
    with suppress(IndexError):
        last.weekly = newsletters.weekly[0].date + datetime.timedelta(days=7)

    # Daily feed: Saves the next day.
    with suppress(IndexError):
        last.daily = newsletters.daily[0].date + datetime.timedelta(days=1)

    return last

semantic_changes(repo, min_date=None)

Extract meaningful changes from a git repository.

Parameters:

Name Type Description Default
repo Repo

Git repository to analyze.

required

Returns:

Name Type Description
changes List[Change]

List of Change objects.

Source code in mkdocs_newsletter/services/git.py
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
def semantic_changes(
    repo: Repo, min_date: Optional[datetime.datetime] = None
) -> List[Change]:
    """Extract meaningful changes from a git repository.

    Args:
        repo: Git repository to analyze.

    Returns:
        changes: List of Change objects.
    """
    now = datetime.datetime.now(tz=tz.tzlocal())
    if min_date is None:
        min_date = datetime.datetime(1800, 1, 1, tzinfo=tz.tzlocal())

    commits = [
        commit
        for commit in repo.iter_commits(rev=repo.head.reference)
        if commit.authored_datetime < now and commit.authored_datetime > min_date
    ]

    return commits_to_changes(commits)