lotek 0.2.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.
Files changed (55) hide show
  1. lotek-0.2.0/PKG-INFO +164 -0
  2. lotek-0.2.0/README.md +139 -0
  3. lotek-0.2.0/lotek/__init__.py +13 -0
  4. lotek-0.2.0/lotek/build.py +41 -0
  5. lotek-0.2.0/lotek/cli.py +99 -0
  6. lotek-0.2.0/lotek/cmd/__init__.py +0 -0
  7. lotek-0.2.0/lotek/cmd/add.py +25 -0
  8. lotek-0.2.0/lotek/cmd/build.py +12 -0
  9. lotek-0.2.0/lotek/cmd/clean.py +12 -0
  10. lotek-0.2.0/lotek/cmd/deploy.py +52 -0
  11. lotek-0.2.0/lotek/cmd/list.py +57 -0
  12. lotek-0.2.0/lotek/cmd/publish.py +63 -0
  13. lotek-0.2.0/lotek/cmd/serve.py +18 -0
  14. lotek-0.2.0/lotek/lib/__init__.py +1 -0
  15. lotek-0.2.0/lotek/lib/about.py +28 -0
  16. lotek-0.2.0/lotek/lib/colors.py +18 -0
  17. lotek-0.2.0/lotek/lib/dirs.py +26 -0
  18. lotek-0.2.0/lotek/lib/frontmatter.py +17 -0
  19. lotek-0.2.0/lotek/lib/highlight.py +25 -0
  20. lotek-0.2.0/lotek/lib/html_stubs.py +23 -0
  21. lotek-0.2.0/lotek/lib/index.py +20 -0
  22. lotek-0.2.0/lotek/lib/init.py +107 -0
  23. lotek-0.2.0/lotek/lib/pages.py +36 -0
  24. lotek-0.2.0/lotek/lib/posts.py +88 -0
  25. lotek-0.2.0/lotek/lib/render.py +65 -0
  26. lotek-0.2.0/lotek/lib/site_config.py +54 -0
  27. lotek-0.2.0/lotek/lib/site_time.py +9 -0
  28. lotek-0.2.0/lotek/lib/static.py +11 -0
  29. lotek-0.2.0/lotek/plugins/__init__.py +1 -0
  30. lotek-0.2.0/lotek/plugins/robots.py +31 -0
  31. lotek-0.2.0/lotek/plugins/rss.py +28 -0
  32. lotek-0.2.0/lotek/site-default.toml +38 -0
  33. lotek-0.2.0/lotek/static/index.html +0 -0
  34. lotek-0.2.0/lotek/static/pygments.css +75 -0
  35. lotek-0.2.0/lotek/static/style.css +264 -0
  36. lotek-0.2.0/lotek/templates/base.html +34 -0
  37. lotek-0.2.0/lotek/templates/feed.xml +10 -0
  38. lotek-0.2.0/lotek/templates/index.html +2 -0
  39. lotek-0.2.0/lotek/templates/post.html +6 -0
  40. lotek-0.2.0/lotek/templates/post.md +7 -0
  41. lotek-0.2.0/lotek.egg-info/PKG-INFO +164 -0
  42. lotek-0.2.0/lotek.egg-info/SOURCES.txt +53 -0
  43. lotek-0.2.0/lotek.egg-info/dependency_links.txt +1 -0
  44. lotek-0.2.0/lotek.egg-info/entry_points.txt +2 -0
  45. lotek-0.2.0/lotek.egg-info/requires.txt +8 -0
  46. lotek-0.2.0/lotek.egg-info/top_level.txt +1 -0
  47. lotek-0.2.0/pyproject.toml +55 -0
  48. lotek-0.2.0/setup.cfg +4 -0
  49. lotek-0.2.0/tests/test_build.py +219 -0
  50. lotek-0.2.0/tests/test_frontmatter.py +54 -0
  51. lotek-0.2.0/tests/test_highlight.py +52 -0
  52. lotek-0.2.0/tests/test_html_stubs.py +111 -0
  53. lotek-0.2.0/tests/test_render.py +113 -0
  54. lotek-0.2.0/tests/test_robots.py +201 -0
  55. lotek-0.2.0/tests/test_rss.py +153 -0
lotek-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,164 @@
1
+ Metadata-Version: 2.4
2
+ Name: lotek
3
+ Version: 0.2.0
4
+ Summary: A static site builder
5
+ Author-email: Brad Arnett <brad.arnett@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://get.lotek.run
8
+ Project-URL: Repository, https://github.com/lotek/lotek.run
9
+ Keywords: static-site,builder,markdown
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Requires-Python: >=3.9
19
+ Description-Content-Type: text/markdown
20
+ Requires-Dist: tomli>=1.2; python_version < "3.11"
21
+ Requires-Dist: markdown>=3.0
22
+ Requires-Dist: pygments>=2.0
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
25
+
26
+ # lotek.run
27
+
28
+ A minimal static blog. Pandoc recommended; falls back to the `markdown` Python module if not available.
29
+
30
+ ## install
31
+
32
+ Install the package:
33
+
34
+ pip install .
35
+
36
+ Or install in development mode:
37
+
38
+ pip install -e .
39
+
40
+ Then use the **lotek** command:
41
+
42
+ Output goes to `output/`. Serve it with literally anything.
43
+
44
+ ## cli
45
+
46
+ For day-to-day post management and operations, use the **lotek** command:
47
+
48
+ lotek init Initialize a new site in the current directory
49
+ lotek build Build the site
50
+ lotek clean Remove build output
51
+ lotek serve [--port N] Serve output locally (default: 8000)
52
+ lotek deploy Build and deploy via rsync (reads .env)
53
+
54
+ lotek list List all posts (formatted table)
55
+ lotek add "Title" Create new post
56
+ lotek publish <slug> Mark a post as published
57
+ lotek unpublish <slug> Mark a post as unpublished
58
+
59
+ All files are human-editable and can be edited directly. It is recommended to preface your post files with a datecode, however it is not directly enforced. Note that the `lotek add` command will automatically add a datecode to the title, as well as providing the frontmatter template needed to be recognized by the lotek command.
60
+
61
+ It might seem weird that the only content editing command lotek exposes is "add". But there's no need for any other tools, because we refuse to enforce their need. With no dependencies come no obligations:
62
+
63
+ Need to delete a file? Use `rm`. Need to search? Use `grep -r`. Want a UI? Use an IDE like Sublime Text or VS Code. Use Obsidian. Don't like any of those options? Roll your own. The pieces are all here and exposed in plain view.
64
+
65
+ ## structure
66
+
67
+ content/posts/ markdown source files (YYYY-MM-DD-slug.md)
68
+ content/pages/ static pages (about.md, now.md, etc.)
69
+ templates/ html/xml templates
70
+ static/ css and any other static assets
71
+ output/ generated site (gitignored)
72
+
73
+ ## frontmatter
74
+
75
+ Posts and pages share the same frontmatter schema:
76
+
77
+ ---
78
+ title: Post Title
79
+ date: YYYY-MM-DD
80
+ tags: tag1, tag2
81
+ publish: true
82
+ ---
83
+
84
+ Set `publish: false` to suppress a file from the build without deleting it. `lotek publish` and `lotek unpublish` manage this field for posts.
85
+
86
+ ## pages
87
+
88
+ Any `.md` file in `content/pages/` is built as a standalone page at `/<slug>.html`. Drop a file, wire up a nav link if you want it linked, done.
89
+
90
+ content/pages/about.md → /about.html
91
+ content/pages/now.md → /now.html
92
+
93
+ `publish: false` in frontmatter suppresses the build, same as posts.
94
+
95
+ ## 404
96
+
97
+ `output/404.html` is generated on every build with full site chrome. Point your server at it:
98
+
99
+ - Apache: `ErrorDocument 404 /404.html`
100
+ - nginx: `error_page 404 /404.html`
101
+
102
+ ## config
103
+
104
+ Site settings live in `site-config.toml`:
105
+
106
+ [site.features]
107
+ robotstxt = true # robots.txt + sitemap generation
108
+ rss = true # RSS feed generation
109
+ skip_future = true # exclude posts with dates > today
110
+
111
+ [site.rss]
112
+ limit = 10
113
+ timezone = "America/Los_Angeles"
114
+
115
+ [site.site]
116
+ title = "lotek.run"
117
+ url = "https://lotek.run"
118
+ description = "dispatches from the margins"
119
+
120
+ [[site.nav]]
121
+ label = "index"
122
+ href = "/"
123
+
124
+ [[site.nav]]
125
+ label = "about"
126
+ href = "/about.html"
127
+
128
+ Nav links are ordered and fully configurable. Add, remove, or reorder `[[site.nav]]` blocks to change what appears in the header. If no nav is configured, the defaults (index, about, rss) are used.
129
+
130
+ ## deploy
131
+
132
+ Set these in `.env` (see `.env.example`):
133
+
134
+ DEPLOY_USER=user
135
+ DEPLOY_HOST=example.com
136
+ DEPLOY_PATH=/var/www/html
137
+
138
+ Then run `lotek deploy`.
139
+
140
+ ## automation
141
+
142
+ I've set up automation via gitea runners personally, but that's out of scope of this project directly. I recommend figuring it out on your own but if I get requests for tutorials I'll probably write about it.
143
+
144
+ ## philosophy
145
+
146
+ No npm. No webpack. No framework. No build chain with a thousand dependencies ready to be poisoned upstream at any goddamn minute.
147
+ Pandoc converts markdown to HTML. A Python script assembles pages from templates.
148
+ The CSS is a flat file. Nothing requires Node.js. TA DA.
149
+
150
+ ## requirements
151
+
152
+ Python 3.9+. Pandoc recommended; falls back to the `markdown` package if not found.
153
+
154
+ If pandoc is not installed, install the fallback:
155
+
156
+ pip install markdown
157
+
158
+ ## getting started
159
+
160
+ mkdir mysite && cd mysite
161
+ lotek init
162
+ # edit site-config.toml with your title/url
163
+ lotek build
164
+ lotek serve
lotek-0.2.0/README.md ADDED
@@ -0,0 +1,139 @@
1
+ # lotek.run
2
+
3
+ A minimal static blog. Pandoc recommended; falls back to the `markdown` Python module if not available.
4
+
5
+ ## install
6
+
7
+ Install the package:
8
+
9
+ pip install .
10
+
11
+ Or install in development mode:
12
+
13
+ pip install -e .
14
+
15
+ Then use the **lotek** command:
16
+
17
+ Output goes to `output/`. Serve it with literally anything.
18
+
19
+ ## cli
20
+
21
+ For day-to-day post management and operations, use the **lotek** command:
22
+
23
+ lotek init Initialize a new site in the current directory
24
+ lotek build Build the site
25
+ lotek clean Remove build output
26
+ lotek serve [--port N] Serve output locally (default: 8000)
27
+ lotek deploy Build and deploy via rsync (reads .env)
28
+
29
+ lotek list List all posts (formatted table)
30
+ lotek add "Title" Create new post
31
+ lotek publish <slug> Mark a post as published
32
+ lotek unpublish <slug> Mark a post as unpublished
33
+
34
+ All files are human-editable and can be edited directly. It is recommended to preface your post files with a datecode, however it is not directly enforced. Note that the `lotek add` command will automatically add a datecode to the title, as well as providing the frontmatter template needed to be recognized by the lotek command.
35
+
36
+ It might seem weird that the only content editing command lotek exposes is "add". But there's no need for any other tools, because we refuse to enforce their need. With no dependencies come no obligations:
37
+
38
+ Need to delete a file? Use `rm`. Need to search? Use `grep -r`. Want a UI? Use an IDE like Sublime Text or VS Code. Use Obsidian. Don't like any of those options? Roll your own. The pieces are all here and exposed in plain view.
39
+
40
+ ## structure
41
+
42
+ content/posts/ markdown source files (YYYY-MM-DD-slug.md)
43
+ content/pages/ static pages (about.md, now.md, etc.)
44
+ templates/ html/xml templates
45
+ static/ css and any other static assets
46
+ output/ generated site (gitignored)
47
+
48
+ ## frontmatter
49
+
50
+ Posts and pages share the same frontmatter schema:
51
+
52
+ ---
53
+ title: Post Title
54
+ date: YYYY-MM-DD
55
+ tags: tag1, tag2
56
+ publish: true
57
+ ---
58
+
59
+ Set `publish: false` to suppress a file from the build without deleting it. `lotek publish` and `lotek unpublish` manage this field for posts.
60
+
61
+ ## pages
62
+
63
+ Any `.md` file in `content/pages/` is built as a standalone page at `/<slug>.html`. Drop a file, wire up a nav link if you want it linked, done.
64
+
65
+ content/pages/about.md → /about.html
66
+ content/pages/now.md → /now.html
67
+
68
+ `publish: false` in frontmatter suppresses the build, same as posts.
69
+
70
+ ## 404
71
+
72
+ `output/404.html` is generated on every build with full site chrome. Point your server at it:
73
+
74
+ - Apache: `ErrorDocument 404 /404.html`
75
+ - nginx: `error_page 404 /404.html`
76
+
77
+ ## config
78
+
79
+ Site settings live in `site-config.toml`:
80
+
81
+ [site.features]
82
+ robotstxt = true # robots.txt + sitemap generation
83
+ rss = true # RSS feed generation
84
+ skip_future = true # exclude posts with dates > today
85
+
86
+ [site.rss]
87
+ limit = 10
88
+ timezone = "America/Los_Angeles"
89
+
90
+ [site.site]
91
+ title = "lotek.run"
92
+ url = "https://lotek.run"
93
+ description = "dispatches from the margins"
94
+
95
+ [[site.nav]]
96
+ label = "index"
97
+ href = "/"
98
+
99
+ [[site.nav]]
100
+ label = "about"
101
+ href = "/about.html"
102
+
103
+ Nav links are ordered and fully configurable. Add, remove, or reorder `[[site.nav]]` blocks to change what appears in the header. If no nav is configured, the defaults (index, about, rss) are used.
104
+
105
+ ## deploy
106
+
107
+ Set these in `.env` (see `.env.example`):
108
+
109
+ DEPLOY_USER=user
110
+ DEPLOY_HOST=example.com
111
+ DEPLOY_PATH=/var/www/html
112
+
113
+ Then run `lotek deploy`.
114
+
115
+ ## automation
116
+
117
+ I've set up automation via gitea runners personally, but that's out of scope of this project directly. I recommend figuring it out on your own but if I get requests for tutorials I'll probably write about it.
118
+
119
+ ## philosophy
120
+
121
+ No npm. No webpack. No framework. No build chain with a thousand dependencies ready to be poisoned upstream at any goddamn minute.
122
+ Pandoc converts markdown to HTML. A Python script assembles pages from templates.
123
+ The CSS is a flat file. Nothing requires Node.js. TA DA.
124
+
125
+ ## requirements
126
+
127
+ Python 3.9+. Pandoc recommended; falls back to the `markdown` package if not found.
128
+
129
+ If pandoc is not installed, install the fallback:
130
+
131
+ pip install markdown
132
+
133
+ ## getting started
134
+
135
+ mkdir mysite && cd mysite
136
+ lotek init
137
+ # edit site-config.toml with your title/url
138
+ lotek build
139
+ lotek serve
@@ -0,0 +1,13 @@
1
+ """lotek.run - A static site builder."""
2
+
3
+ from importlib.metadata import version, PackageNotFoundError
4
+
5
+ try:
6
+ __version__ = version("lotek-run")
7
+ except PackageNotFoundError:
8
+ __version__ = "unknown"
9
+
10
+ import lotek.cli as cli
11
+
12
+ def main():
13
+ cli.main()
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env python3
2
+ """lotek.run -- static site builder
3
+
4
+ Requires: pandoc in PATH or markdown module
5
+ """
6
+
7
+ from lotek.lib.site_config import config
8
+ from lotek.lib.site_time import now_string
9
+ from lotek.lib.pages import generate_pages
10
+ from lotek.lib.posts import generate_posts, load_posts
11
+ from lotek.lib.index import generate_index_landing
12
+ from lotek.lib.dirs import dirs
13
+ from lotek.lib.static import wipe_and_copy_to_output_dir
14
+ from lotek.plugins.rss import generate_rss
15
+ from lotek.plugins.robots import generate_robots
16
+
17
+
18
+ def build():
19
+ """main entry point"""
20
+
21
+ out = dirs.OUTPUT
22
+ print(f"building lotek at {out}")
23
+ out.mkdir(exist_ok=True)
24
+ dirs.OUTPUT_POSTS.mkdir(exist_ok=True)
25
+ dirs.OUTPUT_STATIC.mkdir(exist_ok=True)
26
+ posts = load_posts(dirs.CONTENT_POSTS)
27
+
28
+ generate_posts(posts, out)
29
+ generate_pages(out)
30
+ if config.features.robotstxt:
31
+ print("generating robots.txt...")
32
+ generate_robots(posts, out)
33
+ if config.features.rss:
34
+ print("generating RSS feed...")
35
+ generate_rss(posts, out)
36
+ generate_index_landing(posts, out)
37
+ wipe_and_copy_to_output_dir(out)
38
+
39
+ print(f"built {len(posts)} posts -> output/")
40
+ last_file = out / "_last"
41
+ last_file.write_text(now_string())
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env python3
2
+ """lotek - operational command for lotek.run."""
3
+ import sys
4
+ import argparse
5
+ from pathlib import Path
6
+ import lotek
7
+
8
+ from lotek.lib.init import init
9
+ from lotek.cmd.add import cmd_add
10
+ from lotek.cmd.build import cmd_build
11
+ from lotek.cmd.clean import cmd_clean
12
+ from lotek.cmd.deploy import cmd_deploy
13
+ from lotek.cmd.list import cmd_list
14
+ from lotek.cmd.publish import cmd_publish, cmd_unpublish
15
+ from lotek.cmd.serve import cmd_serve
16
+
17
+ USAGE = f"""
18
+ lotek - Tiny Blog Management Tool
19
+ ver: {getattr(lotek, "__version__", "unknown")}
20
+
21
+ Build:
22
+ lotek build Build the site
23
+ lotek clean Remove build output
24
+ lotek serve [--port N] Serve output locally (default: 8000)
25
+ lotek deploy Build and deploy via rsync (reads .env)
26
+
27
+ Content:
28
+ lotek init Make a new site from scratch
29
+ lotek list List all posts
30
+ lotek add <title> Create new post
31
+ lotek publish <slug> Mark a post as published
32
+ lotek unpublish <slug> Mark a post as unpublished
33
+ """
34
+
35
+ def setup_cmd_parser():
36
+ parser = argparse.ArgumentParser(prog="lotek")
37
+ subs = parser.add_subparsers(dest="command")
38
+
39
+ i = subs.add_parser("init")
40
+ i.add_argument("path", type=str, default=".", nargs="?")
41
+
42
+ subs.add_parser("build")
43
+
44
+ subs.add_parser("clean")
45
+
46
+ p = subs.add_parser("serve")
47
+ p.add_argument("--port", "-p", type=int, default=8000)
48
+
49
+ p = subs.add_parser("deploy")
50
+ p.add_argument("--skip-build", action="store_true")
51
+
52
+
53
+ subs.add_parser("list")
54
+
55
+ p = subs.add_parser("add")
56
+ p.add_argument("title", nargs="?")
57
+
58
+ p = subs.add_parser("publish")
59
+ p.add_argument("slug")
60
+
61
+ p = subs.add_parser("unpublish")
62
+ p.add_argument("slug")
63
+
64
+ args = parser.parse_args()
65
+ return args
66
+
67
+ def main():
68
+ args = setup_cmd_parser()
69
+ if not args.command:
70
+ print(USAGE)
71
+ return 0
72
+ try:
73
+ if args.command == "init":
74
+ return init(Path.absolute(Path(args.path)))
75
+ if args.command == "build":
76
+ return cmd_build()
77
+ if args.command == "clean":
78
+ return cmd_clean()
79
+ if args.command == "serve":
80
+ return cmd_serve(args.port)
81
+ if args.command == "deploy":
82
+ return cmd_deploy(skip_build=args.skip_build)
83
+ if args.command == "list":
84
+ return cmd_list()
85
+ if args.command == "add":
86
+ return cmd_add(args.title)
87
+ if args.command == "publish":
88
+ return cmd_publish(args.slug)
89
+ if args.command == "unpublish":
90
+ return cmd_unpublish(args.slug)
91
+ except KeyboardInterrupt:
92
+ print("\nInterrupted")
93
+ return 1
94
+ except Exception as e:
95
+ print(f"Error: {e}", file=sys.stderr)
96
+ return 1
97
+
98
+ if __name__ == "__main__":
99
+ main()
File without changes
@@ -0,0 +1,25 @@
1
+ from datetime import datetime
2
+ from lotek.lib.colors import red, green
3
+ from lotek.lib.dirs import dirs
4
+
5
+ def cmd_add(title):
6
+ if not title:
7
+ print(red("Title required"))
8
+ return 1
9
+ posts_dir = dirs.CONTENT_POSTS
10
+ posts_dir.mkdir(parents=True, exist_ok=True)
11
+ today = datetime.now().strftime("%Y-%m-%d")
12
+ slug = title.lower().replace(" ", "-")
13
+ fname = f"{today}-{slug}.md"
14
+ fp = posts_dir / fname
15
+ if fp.exists():
16
+ print(red(f"Already exists: {fname}"))
17
+ return 1
18
+ template_path = dirs.TEMPLATES / "post.md"
19
+ if not template_path.exists():
20
+ print(red("Templates not found — run 'lotek init' first"))
21
+ return 1
22
+ template = template_path.read_text()
23
+ fp.write_text(template.replace("{title}", title).replace("{date}", today))
24
+ print(green(f"Created: content/posts/{fname}"))
25
+ return 0
@@ -0,0 +1,12 @@
1
+ import sys
2
+ from lotek.lib.colors import red
3
+ import lotek.build as build_module
4
+
5
+ def cmd_build():
6
+
7
+ try:
8
+ build_module.build()
9
+ return 0
10
+ except Exception as e:
11
+ print(red(f"Build failed: {e}"), file=sys.stderr)
12
+ return 1
@@ -0,0 +1,12 @@
1
+
2
+ import shutil
3
+ from lotek.lib.colors import green
4
+ from lotek.lib.dirs import dirs
5
+
6
+ def cmd_clean():
7
+
8
+ output = dirs.OUTPUT
9
+ if output.exists():
10
+ shutil.rmtree(output)
11
+ print(green("Removed output/"))
12
+ return 0
@@ -0,0 +1,52 @@
1
+ import subprocess
2
+
3
+ from lotek.lib.colors import green, red
4
+ from lotek.lib.dirs import dirs
5
+ from lotek.cmd.build import cmd_build
6
+
7
+ def read_env():
8
+ env_path = dirs.CWD / ".env"
9
+ if not env_path.exists():
10
+ return {}
11
+ env = {}
12
+ for line in env_path.read_text().splitlines():
13
+ if "=" in line and not line.startswith("#"):
14
+ k, _, v = line.partition("=")
15
+ env[k.strip()] = v.strip()
16
+ return env
17
+
18
+
19
+ def cmd_deploy(skip_build=False):
20
+ env = read_env()
21
+ user, host, path = (
22
+ env.get("DEPLOY_USER"),
23
+ env.get("DEPLOY_HOST"),
24
+ env.get("DEPLOY_PATH"),
25
+ )
26
+ if not all([user, host, path]):
27
+ print(red("Missing DEPLOY_USER, DEPLOY_HOST, or DEPLOY_PATH in .env"))
28
+ return 1
29
+ if not skip_build:
30
+ print(green("Building..."))
31
+ rc = cmd_build()
32
+ if rc != 0:
33
+ return rc
34
+ dest = f"{user}@{host}:{path}/"
35
+ print(green(f"Deploying to {dest}"))
36
+ result = subprocess.run(
37
+ [
38
+ "rsync",
39
+ "-avz",
40
+ "--exclude=.env",
41
+ "--exclude=*.pyc",
42
+ "--exclude=__pycache__",
43
+ "--exclude=output",
44
+ "output/",
45
+ dest,
46
+ ]
47
+ )
48
+ if result.returncode != 0:
49
+ print(red("Deploy failed"))
50
+ return result.returncode
51
+ print(green("Deployed successfully"))
52
+ return 0
@@ -0,0 +1,57 @@
1
+ from datetime import datetime
2
+
3
+ from lotek.lib.site_config import config
4
+ from lotek.lib.dirs import dirs
5
+ from lotek.lib.frontmatter import parse_frontmatter
6
+ from lotek.lib.colors import green, BOLD, RESET
7
+
8
+ def _table(headers, rows):
9
+ widths = [len(h) for h in headers]
10
+ for row in rows:
11
+ for i, c in enumerate(row):
12
+ widths[i] = max(widths[i], len(str(c)))
13
+ hdr = "│ " + " │ ".join(h.center(w) for h, w in zip(headers, widths)) + " │"
14
+ print(BOLD + hdr + RESET)
15
+ print("-" * len(hdr))
16
+ for row in rows:
17
+ print("│ " + " │ ".join(str(c).ljust(w) for c, w in zip(row, widths)) + " │")
18
+
19
+ def cmd_list():
20
+ posts_dir = dirs.CONTENT_POSTS
21
+ if not posts_dir.exists():
22
+ print("No posts directory found")
23
+ return 0
24
+ today = datetime.now().date()
25
+ posts = []
26
+ for f in posts_dir.glob("*.md"):
27
+ meta, _ = parse_frontmatter(f.read_text())
28
+ if meta.get("title"):
29
+ posts.append((f, meta))
30
+ if not posts:
31
+ print("No posts found")
32
+ return 0
33
+
34
+ def sort_key(item):
35
+ try:
36
+ return datetime.strptime(item[1].get("date", ""), "%Y-%m-%d").date()
37
+ except ValueError:
38
+ return today
39
+
40
+ posts.sort(key=sort_key, reverse=True)
41
+ rows = []
42
+ for f, meta in posts:
43
+ date_str = meta.get("date", "")
44
+ if meta.get("publish", "").lower() == "false":
45
+ state = "hidden"
46
+ elif config.features.skip_future:
47
+ try:
48
+ d = datetime.strptime(date_str, "%Y-%m-%d").date()
49
+ state = f"in {(d - today).days}d" if d > today else "live"
50
+ except ValueError:
51
+ state = "live"
52
+ else:
53
+ state = "live"
54
+ rows.append([date_str, meta.get("title", ""), f.stem, state])
55
+ print(green(f"{len(posts)} post(s)"))
56
+ _table(["date", "title", "slug", "status"], rows)
57
+ return 0
@@ -0,0 +1,63 @@
1
+ import sys
2
+
3
+ from lotek.lib.frontmatter import parse_frontmatter
4
+ from lotek.lib.colors import green, red
5
+ from lotek.lib.dirs import dirs
6
+
7
+ def _strip_datecode(stem):
8
+ if (
9
+ len(stem) > 11
10
+ and stem.startswith("20")
11
+ and stem[4] == stem[7] == "-"
12
+ and stem[10] == "-"
13
+ ):
14
+ return stem[11:]
15
+ return stem
16
+
17
+ def find_post(slug):
18
+ posts_dir = dirs.CONTENT_POSTS
19
+ if not posts_dir.exists():
20
+ return None
21
+ fp = posts_dir / f"{slug}.md"
22
+ if fp.exists():
23
+ return fp
24
+ for f in posts_dir.glob("*.md"):
25
+ if slug == _strip_datecode(f.stem):
26
+ return f
27
+ matches = [
28
+ f for f in posts_dir.glob("*.md") if _strip_datecode(f.stem).startswith(slug)
29
+ ]
30
+ if len(matches) == 1:
31
+ return matches[0]
32
+ if len(matches) > 1:
33
+ print(red(f"Ambiguous: {len(matches)} matches for '{slug}'"))
34
+ for m in matches:
35
+ print(f" {m.stem}")
36
+ sys.exit(2)
37
+ return None
38
+
39
+ def _set_publish(slug, value):
40
+ fp = find_post(slug)
41
+ if not fp:
42
+ print(red(f"Not found: {slug}"))
43
+ return 1
44
+ meta, body = parse_frontmatter(fp.read_text())
45
+ if not meta.get("title"):
46
+ print(red("No title in frontmatter"))
47
+ return 1
48
+ meta["publish"] = value
49
+ fp.write_text(
50
+ "---\n" + "".join(f"{k}: {v}\n" for k, v in meta.items()) + "---\n\n" + body
51
+ )
52
+ print(green(f"{'Published' if value == 'true' else 'Unpublished'}: {slug}"))
53
+ return 0
54
+
55
+
56
+ def cmd_publish(slug):
57
+ """ i don't know if we even need these, really."""
58
+ return _set_publish(slug, "true")
59
+
60
+
61
+ def cmd_unpublish(slug):
62
+ """ i don't know if we even need these, really."""
63
+ return _set_publish(slug, "false")