daily-writing 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,224 @@
1
+ Metadata-Version: 2.3
2
+ Name: daily-writing
3
+ Version: 0.1.0
4
+ Summary: Python-based static site generator for writing challenges (such as writober)
5
+ Author: Joachim Jablon
6
+ Author-email: Joachim Jablon <ewjoachim@gmail.com>
7
+ Requires-Dist: htpy>=25.10.0
8
+ Requires-Dist: pydantic>=2.12.4
9
+ Requires-Dist: pydantic-extra-types[pycountry]>=2.10.6
10
+ Requires-Dist: pydantic-settings>=2.12.0
11
+ Requires-Dist: brotli>=1.1.0
12
+ Requires-Dist: httpx>=0.28.1
13
+ Requires-Dist: fonttools>=4.60.1
14
+ Requires-Dist: pillow>=11.3.0
15
+ Requires-Dist: numpy>=2.3.4
16
+ Requires-Dist: feedgen>=1.0.0
17
+ Requires-Dist: tzlocal>=5.3.1
18
+ Requires-Dist: babel>=2.17.0
19
+ Requires-Dist: langcodes>=3.5.1
20
+ Requires-Dist: python-frontmatter>=1.1.0
21
+ Requires-Dist: pyyaml>=6.0.3
22
+ Requires-Dist: flowmark>=0.6.1
23
+ Requires-Dist: markdown-it-py[plugins]>=4.0.0
24
+ Requires-Dist: yarl>=1.24.2
25
+ Requires-Dist: typing-extensions>=4.15.0
26
+ Requires-Dist: fastapi>=0.121.0 ; extra == 'server'
27
+ Requires-Dist: pdbpp>=0.11.7 ; extra == 'server'
28
+ Requires-Dist: uvicorn>=0.38.0 ; extra == 'server'
29
+ Requires-Dist: watchfiles>=1.1.1 ; extra == 'server'
30
+ Requires-Dist: websockets>=15.0.1 ; extra == 'server'
31
+ Requires-Python: >=3.14
32
+ Provides-Extra: server
33
+ Description-Content-Type: text/markdown
34
+
35
+ # Daily Writing
36
+
37
+ This repo hosts an open source project that let you create a website presenting
38
+ daily writings, it was created with challenges like Writober or Writever in mind, but
39
+ it's not tied to a specific challenge.
40
+
41
+ Daily Writing mainly turns a bunch of markdown files (one per writing, at most one per
42
+ day), a configuration file, and a directory of static files into a static HTML website
43
+ that can be deployed.
44
+
45
+ Daily Writing offers various features, such as:
46
+ - A page for each writing and a homepage. A menu displaying a calendar with all the
47
+ writing for all months.
48
+ - Each day is linked to a "prompt" (as in writever/writober prompt), and can have a
49
+ title distinct from the prompt. This is useful if the title of your writing isn't
50
+ excatly the prompt you were given for the day. Also, if your prompts are in a given
51
+ language and the title is the prompt you translated to your language, it's nice.
52
+ The prompt will be displayed in the menu over the title.
53
+ - A given writing can span over multiple days and their associated prompts (e.g. if
54
+ you're late), and the calendar will reflect that. Those days may even be
55
+ non-consecutive. It's assumed that the title will be composed of multiple parts each
56
+ related to the associated prompts.
57
+ - Each day in a month is linked to a unique color, and the calendar is colored
58
+ accordingly. The homepage displays all the month's colors.
59
+ - Posts can be written in advance and will only be published of their date.
60
+ - Links to the previous and next writings, for binge-reading.
61
+ - Most metadata can be extracted from the name of the file and the writing markdown
62
+ title, but can then be normalized to frontmatter. Having a frontmatter is necessaryÒ when
63
+ using a CMS editor.
64
+ - Writings spanning multiple days will have a gradient between the colors of the days.
65
+ - A CMS, at `/admin` to update your writings and the site settings directly from your
66
+ browser.
67
+ - A RSS feed.
68
+ - Nice preview images when you post links to social networks.
69
+ - Discreet links to the GitHub source of each writing, to ease online edition or typo
70
+ reporting.
71
+ - Privacy-minded Google fonts, downloaded and optimized at build time so that you can
72
+ get various fonts without offering your reader's data to Google (damn, that was a
73
+ mess to code)
74
+ - Dark mode. No light mode. Contributions welcome on that.
75
+ - Support for extra custom CSS. If you want to add a light mode but just for yourself.
76
+ - Support of various locales, for date formats. Apart from dates and numbers, all the
77
+ words that appear on the interface are configurable, so technically, we support all
78
+ languages that Unicode supports.
79
+ - If you rename a file but want to keep its original URL too, you can add aliases.
80
+
81
+ Author's personal undying love of handcrafted open source and meticulous yak shaving
82
+ went into this project. He likes to think that it shows.
83
+
84
+ ## An example:
85
+
86
+ https://writober.ewjoach.im/ is deployed from https://github.com/ewjoachim/writober,
87
+ using this project.
88
+
89
+ ## Extraction rules
90
+
91
+ From a single file without a frontmatter, these are the informations being extracted.
92
+ Say the file is at `2026/10/03-hello.md` and its content starts with a title that says:
93
+ `# 03 - Bonjour`. The system will find that this writing covers a single day
94
+ (`2026-10-03`), its prompt is "hello" and its official title is "Bonjour".
95
+
96
+ Say the file is at `2026/10/03-hello-darkness.md` and its content starts with a title
97
+ that says: `# 03&04 - Bonjour, Obscurité`. The system will find that this writing covers
98
+ 2 days:
99
+ - `2026-10-03` for which the prompt is `hello` and the title `Bonjour`,
100
+ - `2026-10-04` for which the prompt is `darkness` and the title `Obscurité`.
101
+
102
+ You can always override those values by editing the frontmatter.
103
+
104
+ ## Installation
105
+
106
+ ### Github
107
+
108
+ - [Create a repository from the template](https://repo.new?template_name=daily-writing-template&template_owner=ewjoachim)
109
+ - Adjust configuration with your details
110
+ - Your repository is deployed through GitHub actions (`https://<username>.github.io/<repo name>`)
111
+ - Write your stories
112
+
113
+ ### Elsewhere
114
+
115
+ (Feel free to contribute installation instructions for other platforms).
116
+
117
+ ## Configuration
118
+
119
+ Your website can be configured through either:
120
+
121
+ - `daily-writing.toml`,
122
+ - `pyproject.toml` within the `[tool.daily-writing]` section
123
+ - environments variables,
124
+ - CLI flags.
125
+
126
+ The same options are available in all ways, but they're not spelled out exactly the
127
+ same way. In the toml configuration and environment variables, words are
128
+ separated by `_` (underscores). In the flags, they're separated by `-` (dashes).
129
+ Environment variables are all upper case and prefixed with `DAILY_WRITING_`
130
+
131
+ Examples:
132
+
133
+ - `daily-writing.toml`: `server_url = "https://writober.ewjoach.im"`
134
+ - environments variables: `SERVER_URL="https://writober.ewjoach.im"`
135
+ - CLI flags: `--server-url="https://writober.ewjoach.im"`
136
+
137
+ The easiest and most up-to-date way to learn about every configuration option is
138
+ through: `daily-writing -h`. Feel free to consult [daily-writing.toml](https://github.com/ewjoachim/writober/blob/main/daily-writing.toml) for a working example.
139
+
140
+ > [!NOTE] There is one extra configuration element: setting the `GITHUB_TOKEN`
141
+ > environment variable will help you avoid rate limiting to GitHub's API. It's not added
142
+ > in the normal configuration elements to avoid accidentally committing it to your
143
+ > repository.
144
+
145
+ ## CLI
146
+
147
+ There are 2 main subcommands:
148
+
149
+ - `daily-writing build` builds the website
150
+ - `daily-writing serve` launches a local development server that autobuilds the
151
+ website and auto-reload pages in your browser when changes are detected.
152
+
153
+ ## CI/CD Deployment
154
+
155
+ Solutions like [GitHub Pages](https://docs.github.com/en/pages), [Cloudflare
156
+ Pages](https://developers.cloudflare.com/pages/), [GitLab
157
+ Pages](https://docs.gitlab.com/user/project/pages/), etc will make your life easier, as
158
+ you can edit the markdown files online or push them through git, and have them get
159
+ redeployed automatically daily and upon changes.
160
+
161
+ Here's a GitHub Actions workflow example:
162
+
163
+ ```yaml
164
+ name: Deploy
165
+
166
+ on:
167
+ push:
168
+ branches: ["main"]
169
+ workflow_dispatch:
170
+ schedule:
171
+ - cron: "1 22,23 * 10 *" # Adjust to whenever relvant to you
172
+
173
+ permissions:
174
+ contents: read
175
+ pages: write
176
+ id-token: write
177
+
178
+ concurrency:
179
+ group: "pages"
180
+ cancel-in-progress: false
181
+
182
+ jobs:
183
+ build:
184
+ runs-on: ubuntu-latest
185
+ steps:
186
+ - uses: actions/checkout@v5
187
+
188
+ - name: Setup Pages
189
+ uses: actions/configure-pages@v5
190
+
191
+ - uses: astral-sh/setup-uv@v6
192
+
193
+ - name: Sphinx build
194
+ run: uv run daily-writing build
195
+
196
+ - name: Upload artifact
197
+ uses: actions/upload-pages-artifact@v4
198
+ with:
199
+ path: _build
200
+
201
+ # Deployment job
202
+ deploy:
203
+ environment:
204
+ name: github-pages
205
+ url: ${{ steps.deployment.outputs.page_url }}
206
+ runs-on: ubuntu-latest
207
+ needs: build
208
+ steps:
209
+ - name: Deploy to GitHub Pages
210
+ id: deployment
211
+ uses: actions/deploy-pages@v4
212
+ ```
213
+
214
+ ## Adding new content
215
+
216
+ ### Manual
217
+ Add and edit your markdown files in the repository via Git or via your
218
+ platform's Web interface.
219
+
220
+ ### Online
221
+ Daily-Writing can come with a [CMS](https://github.com/sveltia/sveltia-cms), allowing
222
+ you to get a nice admin interface that doesn't require to interact with Git.
223
+
224
+ Integration is not yet 100% streamlined, but it's coming.
@@ -0,0 +1,190 @@
1
+ # Daily Writing
2
+
3
+ This repo hosts an open source project that let you create a website presenting
4
+ daily writings, it was created with challenges like Writober or Writever in mind, but
5
+ it's not tied to a specific challenge.
6
+
7
+ Daily Writing mainly turns a bunch of markdown files (one per writing, at most one per
8
+ day), a configuration file, and a directory of static files into a static HTML website
9
+ that can be deployed.
10
+
11
+ Daily Writing offers various features, such as:
12
+ - A page for each writing and a homepage. A menu displaying a calendar with all the
13
+ writing for all months.
14
+ - Each day is linked to a "prompt" (as in writever/writober prompt), and can have a
15
+ title distinct from the prompt. This is useful if the title of your writing isn't
16
+ excatly the prompt you were given for the day. Also, if your prompts are in a given
17
+ language and the title is the prompt you translated to your language, it's nice.
18
+ The prompt will be displayed in the menu over the title.
19
+ - A given writing can span over multiple days and their associated prompts (e.g. if
20
+ you're late), and the calendar will reflect that. Those days may even be
21
+ non-consecutive. It's assumed that the title will be composed of multiple parts each
22
+ related to the associated prompts.
23
+ - Each day in a month is linked to a unique color, and the calendar is colored
24
+ accordingly. The homepage displays all the month's colors.
25
+ - Posts can be written in advance and will only be published of their date.
26
+ - Links to the previous and next writings, for binge-reading.
27
+ - Most metadata can be extracted from the name of the file and the writing markdown
28
+ title, but can then be normalized to frontmatter. Having a frontmatter is necessaryÒ when
29
+ using a CMS editor.
30
+ - Writings spanning multiple days will have a gradient between the colors of the days.
31
+ - A CMS, at `/admin` to update your writings and the site settings directly from your
32
+ browser.
33
+ - A RSS feed.
34
+ - Nice preview images when you post links to social networks.
35
+ - Discreet links to the GitHub source of each writing, to ease online edition or typo
36
+ reporting.
37
+ - Privacy-minded Google fonts, downloaded and optimized at build time so that you can
38
+ get various fonts without offering your reader's data to Google (damn, that was a
39
+ mess to code)
40
+ - Dark mode. No light mode. Contributions welcome on that.
41
+ - Support for extra custom CSS. If you want to add a light mode but just for yourself.
42
+ - Support of various locales, for date formats. Apart from dates and numbers, all the
43
+ words that appear on the interface are configurable, so technically, we support all
44
+ languages that Unicode supports.
45
+ - If you rename a file but want to keep its original URL too, you can add aliases.
46
+
47
+ Author's personal undying love of handcrafted open source and meticulous yak shaving
48
+ went into this project. He likes to think that it shows.
49
+
50
+ ## An example:
51
+
52
+ https://writober.ewjoach.im/ is deployed from https://github.com/ewjoachim/writober,
53
+ using this project.
54
+
55
+ ## Extraction rules
56
+
57
+ From a single file without a frontmatter, these are the informations being extracted.
58
+ Say the file is at `2026/10/03-hello.md` and its content starts with a title that says:
59
+ `# 03 - Bonjour`. The system will find that this writing covers a single day
60
+ (`2026-10-03`), its prompt is "hello" and its official title is "Bonjour".
61
+
62
+ Say the file is at `2026/10/03-hello-darkness.md` and its content starts with a title
63
+ that says: `# 03&04 - Bonjour, Obscurité`. The system will find that this writing covers
64
+ 2 days:
65
+ - `2026-10-03` for which the prompt is `hello` and the title `Bonjour`,
66
+ - `2026-10-04` for which the prompt is `darkness` and the title `Obscurité`.
67
+
68
+ You can always override those values by editing the frontmatter.
69
+
70
+ ## Installation
71
+
72
+ ### Github
73
+
74
+ - [Create a repository from the template](https://repo.new?template_name=daily-writing-template&template_owner=ewjoachim)
75
+ - Adjust configuration with your details
76
+ - Your repository is deployed through GitHub actions (`https://<username>.github.io/<repo name>`)
77
+ - Write your stories
78
+
79
+ ### Elsewhere
80
+
81
+ (Feel free to contribute installation instructions for other platforms).
82
+
83
+ ## Configuration
84
+
85
+ Your website can be configured through either:
86
+
87
+ - `daily-writing.toml`,
88
+ - `pyproject.toml` within the `[tool.daily-writing]` section
89
+ - environments variables,
90
+ - CLI flags.
91
+
92
+ The same options are available in all ways, but they're not spelled out exactly the
93
+ same way. In the toml configuration and environment variables, words are
94
+ separated by `_` (underscores). In the flags, they're separated by `-` (dashes).
95
+ Environment variables are all upper case and prefixed with `DAILY_WRITING_`
96
+
97
+ Examples:
98
+
99
+ - `daily-writing.toml`: `server_url = "https://writober.ewjoach.im"`
100
+ - environments variables: `SERVER_URL="https://writober.ewjoach.im"`
101
+ - CLI flags: `--server-url="https://writober.ewjoach.im"`
102
+
103
+ The easiest and most up-to-date way to learn about every configuration option is
104
+ through: `daily-writing -h`. Feel free to consult [daily-writing.toml](https://github.com/ewjoachim/writober/blob/main/daily-writing.toml) for a working example.
105
+
106
+ > [!NOTE] There is one extra configuration element: setting the `GITHUB_TOKEN`
107
+ > environment variable will help you avoid rate limiting to GitHub's API. It's not added
108
+ > in the normal configuration elements to avoid accidentally committing it to your
109
+ > repository.
110
+
111
+ ## CLI
112
+
113
+ There are 2 main subcommands:
114
+
115
+ - `daily-writing build` builds the website
116
+ - `daily-writing serve` launches a local development server that autobuilds the
117
+ website and auto-reload pages in your browser when changes are detected.
118
+
119
+ ## CI/CD Deployment
120
+
121
+ Solutions like [GitHub Pages](https://docs.github.com/en/pages), [Cloudflare
122
+ Pages](https://developers.cloudflare.com/pages/), [GitLab
123
+ Pages](https://docs.gitlab.com/user/project/pages/), etc will make your life easier, as
124
+ you can edit the markdown files online or push them through git, and have them get
125
+ redeployed automatically daily and upon changes.
126
+
127
+ Here's a GitHub Actions workflow example:
128
+
129
+ ```yaml
130
+ name: Deploy
131
+
132
+ on:
133
+ push:
134
+ branches: ["main"]
135
+ workflow_dispatch:
136
+ schedule:
137
+ - cron: "1 22,23 * 10 *" # Adjust to whenever relvant to you
138
+
139
+ permissions:
140
+ contents: read
141
+ pages: write
142
+ id-token: write
143
+
144
+ concurrency:
145
+ group: "pages"
146
+ cancel-in-progress: false
147
+
148
+ jobs:
149
+ build:
150
+ runs-on: ubuntu-latest
151
+ steps:
152
+ - uses: actions/checkout@v5
153
+
154
+ - name: Setup Pages
155
+ uses: actions/configure-pages@v5
156
+
157
+ - uses: astral-sh/setup-uv@v6
158
+
159
+ - name: Sphinx build
160
+ run: uv run daily-writing build
161
+
162
+ - name: Upload artifact
163
+ uses: actions/upload-pages-artifact@v4
164
+ with:
165
+ path: _build
166
+
167
+ # Deployment job
168
+ deploy:
169
+ environment:
170
+ name: github-pages
171
+ url: ${{ steps.deployment.outputs.page_url }}
172
+ runs-on: ubuntu-latest
173
+ needs: build
174
+ steps:
175
+ - name: Deploy to GitHub Pages
176
+ id: deployment
177
+ uses: actions/deploy-pages@v4
178
+ ```
179
+
180
+ ## Adding new content
181
+
182
+ ### Manual
183
+ Add and edit your markdown files in the repository via Git or via your
184
+ platform's Web interface.
185
+
186
+ ### Online
187
+ Daily-Writing can come with a [CMS](https://github.com/sveltia/sveltia-cms), allowing
188
+ you to get a nice admin interface that doesn't require to interact with Git.
189
+
190
+ Integration is not yet 100% streamlined, but it's coming.
File without changes
@@ -0,0 +1,44 @@
1
+ import logging
2
+
3
+ from . import build, normalize
4
+ from . import settings as settings_module
5
+
6
+
7
+ class NoCommandSelected(Exception):
8
+ pass
9
+
10
+
11
+ class MissingExtraDependency(Exception):
12
+ pass
13
+
14
+
15
+ def main():
16
+ settings = settings_module.Settings() # pyright: ignore[reportCallIssue]
17
+ logging.basicConfig(level=settings.verbosity)
18
+ logging.getLogger("fontTools").setLevel("WARNING")
19
+ logging.getLogger("markdown_it.rules_block").setLevel("INFO")
20
+
21
+ match settings.subcommand:
22
+ case settings_module.Build():
23
+ build.build(settings=settings)
24
+ case settings_module.Serve():
25
+ serve_website(settings=settings)
26
+ case settings_module.Normalize():
27
+ normalize.normalize(settings=settings)
28
+ case None:
29
+ raise NoCommandSelected("No command selected")
30
+
31
+
32
+ def serve_website(settings: settings_module.Settings):
33
+ try:
34
+ from . import serve # noqa: PLC0415
35
+ except ImportError as exc:
36
+ raise MissingExtraDependency(
37
+ "Extra dependencies `server` is required for daily-writing serve (`pip install daily-writing[server]`)"
38
+ ) from exc
39
+
40
+ serve.serve(settings=settings)
41
+
42
+
43
+ if __name__ == "__main__":
44
+ main()
@@ -0,0 +1,58 @@
1
+ import io
2
+ import pathlib
3
+ from typing import Protocol, override
4
+
5
+ import pydantic
6
+ from pydantic import dataclasses as pdataclasses
7
+
8
+
9
+ class BaseArtifact(Protocol):
10
+ def write(self, destination: pathlib.Path): ...
11
+
12
+
13
+ @pdataclasses.dataclass(kw_only=True)
14
+ class TextArtifact:
15
+ path: pathlib.Path
16
+ contents: str
17
+
18
+ def write(self, destination: pathlib.Path):
19
+ if self.path.is_absolute():
20
+ raise ValueError("Only relative paths are accepted")
21
+
22
+ (destination / self.path).parent.mkdir(exist_ok=True, parents=True)
23
+ (destination / self.path).write_text(self.contents)
24
+
25
+
26
+ @pdataclasses.dataclass(kw_only=True)
27
+ class HTMLArtifact(TextArtifact):
28
+ @override
29
+ def write(self, destination: pathlib.Path):
30
+ super().write(destination=destination)
31
+
32
+
33
+ @pdataclasses.dataclass(
34
+ kw_only=True, config=pydantic.ConfigDict(arbitrary_types_allowed=True)
35
+ )
36
+ class BytesArtifact:
37
+ path: pathlib.Path
38
+ contents: io.BytesIO
39
+
40
+ def write(self, destination: pathlib.Path):
41
+ if self.path.is_absolute():
42
+ raise ValueError("Only relative paths are accepted")
43
+
44
+ (destination / self.path).parent.mkdir(exist_ok=True, parents=True)
45
+ (destination / self.path).write_bytes(self.contents.getvalue())
46
+
47
+
48
+ @pdataclasses.dataclass(kw_only=True)
49
+ class FileArtifact:
50
+ path: pathlib.Path
51
+ source: pathlib.Path
52
+ destination: pathlib.Path
53
+
54
+ def write(self, destination: pathlib.Path):
55
+ rel = self.path.relative_to(self.source)
56
+ destination = destination / self.destination / rel
57
+ destination.parent.mkdir(exist_ok=True, parents=True)
58
+ self.path.copy(destination)
@@ -0,0 +1,51 @@
1
+ import datetime
2
+ import io
3
+ import pathlib
4
+ import zoneinfo
5
+ from typing import cast
6
+
7
+ from feedgen.feed import FeedGenerator
8
+ from pydantic import dataclasses as pdataclasses
9
+
10
+ from daily_writing import artifacts
11
+
12
+ from . import settings as settings_module
13
+
14
+
15
+ @pdataclasses.dataclass(kw_only=True)
16
+ class FeedEntryArtifact:
17
+ artifact_id: str
18
+ title: str
19
+ link: str
20
+ date: datetime.date
21
+
22
+
23
+ class Feed:
24
+ def __init__(self, settings: settings_module.Settings):
25
+ self.feed_gen: FeedGenerator = FeedGenerator()
26
+ self.feed_gen.id(str(settings.site_full_url))
27
+ self.feed_gen.title(settings.site_name)
28
+ self.feed_gen.author(name=settings.author)
29
+ self.feed_gen.link(href=str(settings.site_full_url), rel="self")
30
+ self.feed_gen.subtitle(settings.description)
31
+ self.feed_gen.language(str(settings.locale.locale))
32
+ self.timezone: str = settings.timezone
33
+ self.atom_path: pathlib.Path = settings.atom_path
34
+
35
+ def add_entry(self, item_id: str, title: str, link: str, date: datetime.date):
36
+ entry = self.feed_gen.add_entry() # pyright: ignore[reportUnknownVariableType]
37
+ entry.id(item_id)
38
+ entry.title(title)
39
+ entry.link(href=link)
40
+ entry.updated(
41
+ datetime.datetime.combine(
42
+ date, datetime.time(), tzinfo=zoneinfo.ZoneInfo(self.timezone)
43
+ ).isoformat()
44
+ )
45
+
46
+ def get_artifact(self) -> artifacts.BytesArtifact:
47
+ contents: bytes = cast(bytes, self.feed_gen.atom_str(pretty=True))
48
+
49
+ return artifacts.BytesArtifact(
50
+ path=self.atom_path, contents=io.BytesIO(contents)
51
+ )